diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 4e82725a7fb0..c2e9fa9cef24 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -1,46 +1,80 @@ -# Contributor Covenant Code of Conduct +# Magento Code of Conduct ## Our Pledge -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our project and community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + ## Our Standards -Examples of behavior that contributes to creating a positive environment include: +Examples of behavior that contribute to a positive environment for our project and community include: + + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best, not just for us as individuals but for the overall community -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* The use of sexualized language or imagery and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Publishing others’ private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting + ## Our Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. +Project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any instances of unacceptable behavior. -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. +This Code of Conduct applies when an individual is representing the project or its community both within project spaces and in public spaces. Examples of representing a project or community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at engcom@magento.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by first contacting the project team at engcom@adobe.com. Oversight of Adobe projects is handled by the Adobe Open Source Office, which has final say in any violations and enforcement of this Code of Conduct and can be reached at Grp-opensourceoffice@adobe.com. All complaints will be reviewed and investigated promptly and fairly. -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. +The project team must respect the privacy and security of the reporter of any incident. -## Attribution +Project maintainers who do not follow or enforce the Code of Conduct may face temporary or permanent repercussions as determined by other members of the project's leadership or the Adobe Open Source Office. + + +## Enforcement Guidelines + +Project maintainers will follow these Community Impact Guidelines in determining the consequences for any action they deem to be in violation of this Code of Conduct: + +### 1. Correction + +Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. +Consequence: A private, written warning from project maintainers describing the violation and why the behavior was unacceptable. A public apology may be requested from the violator before any further involvement in the project by violator. -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] +### 2. Warning + +Community Impact: A relatively minor violation through a single incident or series of actions. + +Consequence: A written warning from project maintainers that includes stated consequences for continued unacceptable behavior. Violator must refrain from interacting with the people involved for a specified period of time as determined by the project maintainers, including, but not limited to, unsolicited interaction with those enforcing the Code of Conduct through channels such as community spaces and social media. Continued violations may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +Community Impact: A more serious violation of community standards, including sustained unacceptable behavior. + +Consequence: A temporary ban from any interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Failure to comply with the temporary ban may lead to a permanent ban. + +### 4. Permanent Ban + +Community Impact: Demonstrating a consistent pattern of violation of community standards or an egregious violation of community standards, including, but not limited to, sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +Consequence: A permanent ban from any interaction with the community. + + +## Attribution -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 61e14fd02b22..ec7ddb4085f2 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/.github/workflows/coding-standard-baseline.yml b/.github/workflows/coding-standard-baseline.yml new file mode 100644 index 000000000000..cc7d9cfc61fe --- /dev/null +++ b/.github/workflows/coding-standard-baseline.yml @@ -0,0 +1,14 @@ +name: Coding Standard With Baseline +on: + pull_request: + branches: + - 2.4-develop +permissions: + contents: read +jobs: + coding-standard: + runs-on: ubuntu-latest + steps: + - name: Run Coding Standard Baseline + uses: mage-os/github-actions/coding-standard-baseline@main + diff --git a/.github/workflows/coding-standard.yml b/.github/workflows/coding-standard.yml deleted file mode 100644 index 512b23f8bddd..000000000000 --- a/.github/workflows/coding-standard.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Coding Standard - -on: - pull_request: - branches: - - 2.4-develop - -permissions: - contents: read - -jobs: - coding-standard: - runs-on: ubuntu-latest - steps: - - name: Run Coding Standard - uses: graycoreio/github-actions-magento2/coding-standard@main diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 000000000000..dd29dfcb9b02 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,13 @@ +name: Unit Tests +run-name: ${{ github.actor }} is running Unit Tests +on: + pull_request: + branches: + - 2.4-develop + +permissions: + contents: write + +jobs: + run-unit-tests: + uses: mage-os/infrastructure/.github/workflows/unit-tests.yml@main diff --git a/README.md b/README.md index 46c9fc128c00..a02a955a9ebb 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://raw.githubusercontent.com/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 8fbe2f770f53..a7aea8094f81 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 c620230282e1..000000000000 --- 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 e3c5d1120285..000000000000 --- 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 7fd8a59c255d..000000000000 --- 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 3568f17e1215..000000000000 --- 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 f299c87e35fd..000000000000 --- 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 036a1abe01f8..000000000000 --- 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 6fe3a8c6aeca..000000000000 --- 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 93ea97959ec1..000000000000 --- 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 10d43b155276..000000000000 --- 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 209b2078b175..000000000000 --- 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 a3435ef5f13d..000000000000 --- 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 2c651543acd7..000000000000 --- 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 0c3d13fab3e9..000000000000 --- 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 c85f138669dc..000000000000 --- 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 9ee688720bed..000000000000 --- 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 e2c9b93cf7b1..000000000000 --- 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 23239e382ac1..000000000000 --- 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 e4f80e1ed926..000000000000 --- 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 960bcf92bb52..000000000000 --- 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 142de4df4d57..000000000000 --- 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 4ced72c6b754..000000000000 --- 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 3a8c7648ed08..000000000000 --- 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 f1caba6fa5b4..000000000000 --- 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 f1594f55f61b..000000000000 --- 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 d2784c5bed3c..000000000000 --- 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 8f33a5abc338..000000000000 --- 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 43183f10f6eb..000000000000 --- 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 b8ec54c171af..000000000000 --- 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 cc3087d02b25..000000000000 --- 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 5c2ff966f05a..000000000000 --- 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 e2cc99e5a4cc..000000000000 --- 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 afded17121c5..000000000000 --- 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 bb0cf7bf1257..000000000000 --- 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 a6eb22aeaafb..000000000000 --- 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 32255faf3450..000000000000 --- 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 543e3940d770..000000000000 --- 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 95c274bf1418..000000000000 --- 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 434a953544db..000000000000 --- 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 b781f3f87429..000000000000 --- 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 95fdc5e5f025..000000000000 --- 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 b5c134d91da3..000000000000 --- 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 eab147edea7e..000000000000 --- 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 9a6656e269fa..000000000000 --- 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 52ae88888507..000000000000 --- 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 ac2c7d58aa77..000000000000 --- 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 b7db1ef86b81..000000000000 --- 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 2465c6dcd6d6..000000000000 --- 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 9e501a10e7eb..000000000000 --- 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 7cc18b4e213b..000000000000 --- 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 4e1b49465821..000000000000 --- 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 40eee6271dd0..000000000000 --- 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 168b69e37cdd..000000000000 --- 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 66ecb1cea73a..000000000000 --- 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 e8a7f74f3f56..000000000000 --- 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 ea4bfa01797d..000000000000 --- 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 461ac95d7aec..000000000000 --- 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 b1ad7bbc19a5..000000000000 --- 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 6face4e362e5..000000000000 --- 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 8a62286b8d74..000000000000 --- 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 37a96b53655f..000000000000 --- 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 ce0c16b4b1bb..000000000000 --- 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 d27a7339ee3e..000000000000 --- 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 a9b416204c68..000000000000 --- 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 e859269305cf..000000000000 --- 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 abeaed22c225..000000000000 --- 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 a682821c5bac..000000000000 --- 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 973b8f8e260c..000000000000 --- 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 ca809e24f3db..000000000000 --- 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 d083777145e8..000000000000 --- 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 c46364f26501..000000000000 --- 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 7dedfd45631e..000000000000 --- 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 526713a9b104..000000000000 --- 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 2eec8d1de225..000000000000 --- 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 ea584f6aab66..000000000000 --- 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 3581319c86b7..000000000000 --- 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 acd65c5e342e..000000000000 --- 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 8dcdb8b8b8ba..000000000000 --- 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 fd1e82884f66..000000000000 --- 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 d542a273391d..000000000000 --- 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 94cdfbedf006..000000000000 --- 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 864e425b053f..000000000000 --- 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 4f2309b35d23..000000000000 --- 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 8313183713e6..000000000000 --- 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 68ba28fdaa69..000000000000 --- 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 195b4be7c930..000000000000 --- 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 a5d530e42594..000000000000 --- 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 89a6a7da699a..000000000000 --- 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 b6804a78d1da..000000000000 --- 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 3a86352264d7..000000000000 --- 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 30537aaa20f2..000000000000 --- 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 cfbd91534bd2..000000000000 --- 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 16c780240a03..000000000000 --- 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 859432ee4551..000000000000 --- 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 857cf2efbdbd..000000000000 --- 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 c5e3929e8e4b..000000000000 --- 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 5d05d7f8281c..000000000000 --- 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 623d2ceb77a0..000000000000 --- 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 d31abbf60219..000000000000 --- 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 388cc7309ca9..000000000000 --- 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 a01a8bd4921d..000000000000 --- 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 7582650a3285..000000000000 --- 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 6d338b5bd608..000000000000 --- 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 aadf389b8db4..000000000000 --- 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 d73050ce38b6..000000000000 --- 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 5da3e654b2e7..000000000000 --- 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 f018821683de..000000000000 --- 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 d2ce344d23c4..000000000000 --- 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 8f54b888f64a..000000000000 --- 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 efcd60d42ab0..000000000000 --- 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 efcd60d42ab0..000000000000 --- 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 2f62e7c9109d..000000000000 --- 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 81fe72eb260c..000000000000 --- 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 d6ccc2c6ab16..000000000000 --- 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 3207291322cf..000000000000 --- 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 9e24887140f1..000000000000 --- 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 4464c0b9635f..000000000000 --- 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 d4c6a922a589..000000000000 --- 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 4464c0b9635f..000000000000 --- 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 4464c0b9635f..000000000000 --- 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 595e56b8e50d..000000000000 --- 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 3505173a4f2b..000000000000 --- 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 10ef59fe2712..000000000000 --- 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 26d63b69bdcc..000000000000 --- 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 9f0f1553a14d..000000000000 --- 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 ed2e2067fa44..000000000000 --- 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 6e8968607d66..000000000000 --- 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 f75940c29bf2..000000000000 --- 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 6db4f0175238..000000000000 --- 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 88d8f1c393fd..000000000000 --- 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 71e47555f7f9..000000000000 --- 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 396c9bd39fca..000000000000 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 c5ddacefb919..000000000000 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 61278e3b84e7..000000000000 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 8118aab5303e..000000000000 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 da2aa4844b89..000000000000 --- 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 a13e8ab14779..000000000000 --- 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 f9538d07f5d8..000000000000 --- 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 289b43176fdf..000000000000 --- 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 dba1e05dff2c..000000000000 --- 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 45b4640c7116..000000000000 --- 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 a90432f4cb6f..000000000000 --- 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 a94d1aaca571..000000000000 --- 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 b0cce48fe003..000000000000 --- 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 8474b7feeec9..000000000000 --- 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 e905344031ad..65a9e159f7ae 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 fa17e425e13d..90a0c5890f04 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 b8196c8ae090..dfac97747cb3 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 2967aa9ac60b..d94604f1b714 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 eb24b074bbff..f3db9f1fd263 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 1ab277b4f788..53daccc815e5 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 34f85625c030..000000000000 --- 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 dcbe1f6e94f2..000000000000 --- 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 26b448106aa2..000000000000 --- 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 35afea841c3e..000000000000 --- 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 2d32870c7312..000000000000 --- 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 49525fd99da9..000000000000 --- 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 f39d641b18a1..000000000000 --- 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 6e6999d740bb..000000000000 --- 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 278ffbc2ca0f..000000000000 --- 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 c7af6a408203..000000000000 --- 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 4a5c4a49b9b9..000000000000 --- 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 5a9274d80682..000000000000 --- 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 c325f8aa78f4..000000000000 --- 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 09723a6c7209..000000000000 --- 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 e457e61aeb58..000000000000 --- 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 79bf6042a46f..000000000000 --- 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 16f410d0c27d..000000000000 --- 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 d70eef19d2b6..000000000000 --- 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 56822a2284e1..000000000000 --- 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 705f52fc5e2b..000000000000 --- 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 a62617529ffb..000000000000 --- 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 68347d5afb20..000000000000 --- 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 48eb8a29a69a..000000000000 --- 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 f1ef348654fa..000000000000 --- 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 6cf84602fb38..000000000000 --- 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 40c7d3c692ff..000000000000 --- 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 19d1ac19c6d0..000000000000 --- 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 987d04c9f306..000000000000 --- 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 b7d07b33f326..000000000000 --- 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 7218df060d95..000000000000 --- 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 aee0ef226ea8..000000000000 --- 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 776da1fb4e59..000000000000 --- 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 d15dd8e3ed23..000000000000 --- 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 704d791f1bc0..000000000000 --- 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 348316986b20..000000000000 --- 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 a0b3d696ca51..000000000000 --- 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 a6fd4d9655c3..000000000000 --- 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 db1033a511fc..000000000000 --- 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 fefac94c05d3..000000000000 --- 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 8483e2043759..000000000000 --- 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 b98b19f39826..000000000000 --- 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 6bede456f5bd..000000000000 --- 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 6435893a4566..000000000000 --- 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 14e2c07bfc40..000000000000 --- 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 a2c7efd6a6b6..000000000000 --- 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 8ac45f9f4434..000000000000 --- 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 326f727e7a2d..000000000000 --- 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 268222290c51..000000000000 --- 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 9a3d8f27a87d..000000000000 --- 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 f80868022a67..000000000000 --- 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 5f4caac410da..000000000000 --- 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 30b4d3a98b3d..000000000000 --- 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 bec85f0c7f97..000000000000 --- 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 d110b1fafa36..000000000000 --- 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 03601668e4b8..000000000000 --- 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 97d4cf75aa0a..000000000000 --- 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 b19b836ba989..000000000000 --- 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 f3fd3362ad9c..000000000000 --- 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 bdf728587220..000000000000 --- 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 01eca816fb3c..000000000000 --- 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 07c0ab8951b3..000000000000 --- 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 53386decafa9..000000000000 --- 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 ed6c1b7c8a9c..000000000000 --- 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 2c2cabe3ab4e..000000000000 --- 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 7a403e14baa6..000000000000 --- 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 dae814b30718..000000000000 --- 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 cd851950eca7..000000000000 --- 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 d1717fefee99..000000000000 --- 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 a22b9008131c..000000000000 --- 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 ded53b81d330..000000000000 --- 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 92da1a193dea..000000000000 --- 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 79b731219bc1..000000000000 --- 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 2e19ce903c78..000000000000 --- 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 83b8d7171839..000000000000 --- 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 9cad49e7e5e9..000000000000 --- 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 18a6ad2bcce7..000000000000 --- 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 604c053f1b2a..000000000000 --- 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 4e96181cc5c3..000000000000 --- 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 bc8200e9e641..000000000000 --- 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 3860a31c1373..000000000000 --- 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 5027ed6589ef..000000000000 --- 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 42a70977f608..000000000000 --- 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 8c18b02a5494..000000000000 --- 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 e59b7e883369..000000000000 --- 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 49525fd99da9..000000000000 --- 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 f39d641b18a1..000000000000 --- 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 49442a872f7d..000000000000 --- 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 13a02442e5c9..000000000000 --- 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 2ec4c518b9ec..000000000000 --- 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 af0df625f432..000000000000 --- 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 b389eabb341a..2160b55ddad4 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/Helper/Data.php b/app/code/Magento/AdvancedSearch/Helper/Data.php new file mode 100644 index 000000000000..951647277346 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Helper/Data.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AdvancedSearch\Helper; + +use Magento\Framework\App\Helper\Context; +use Magento\Framework\Search\EngineResolverInterface; +use Magento\Framework\App\Helper\AbstractHelper; +use OpenSearch\Client; + +class Data extends AbstractHelper +{ + + public const OPENSEARCH = 'opensearch'; + public const MAJOR_VERSION = '2'; + + /** + * @var EngineResolverInterface + */ + public $engineResolver; + + /** + * @param Context $context + * @param EngineResolverInterface $engineResolver + */ + public function __construct( + Context $context, + EngineResolverInterface $engineResolver + ) { + parent::__construct($context); + $this->engineResolver = $engineResolver; + } + + /** + * Check if opensearch v2.x + * + * @return bool + */ + public function isClientOpenSearchV2(): bool + { + $searchEngine = $this->engineResolver->getCurrentSearchEngine(); + if (stripos($searchEngine, self::OPENSEARCH) !== false) { + if (substr(Client::VERSION, 0, 1) == self::MAJOR_VERSION) { + return true; + } + } + return false; + } +} diff --git a/app/code/Magento/AdvancedSearch/Model/Client/ClientFactory.php b/app/code/Magento/AdvancedSearch/Model/Client/ClientFactory.php index 05eb513d6839..70ff445fb2b6 100644 --- a/app/code/Magento/AdvancedSearch/Model/Client/ClientFactory.php +++ b/app/code/Magento/AdvancedSearch/Model/Client/ClientFactory.php @@ -6,11 +6,12 @@ namespace Magento\AdvancedSearch\Model\Client; use Magento\Framework\ObjectManagerInterface; +use Magento\AdvancedSearch\Helper\Data; class ClientFactory implements ClientFactoryInterface { /** - * Object manager + * Object var * * @var ObjectManagerInterface */ @@ -21,14 +22,32 @@ class ClientFactory implements ClientFactoryInterface */ private $clientClass; + /** + * @var string + */ + private $openSearch; + + /** + * @var Data + */ + protected $helper; + /** * @param ObjectManagerInterface $objectManager * @param string $clientClass + * @param Data $helper + * @param string|null $openSearch */ - public function __construct(ObjectManagerInterface $objectManager, $clientClass) - { + public function __construct( + ObjectManagerInterface $objectManager, + $clientClass, + Data $helper, + $openSearch = null + ) { $this->objectManager = $objectManager; $this->clientClass = $clientClass; + $this->openSearch = $openSearch; + $this->helper = $helper; } /** @@ -39,8 +58,13 @@ public function __construct(ObjectManagerInterface $objectManager, $clientClass) */ public function create(array $options = []) { + $class = $this->clientClass; + if ($this->helper->isClientOpenSearchV2()) { + $class = $this->openSearch; + } + return $this->objectManager->create( - $this->clientClass, + $class, ['options' => $options] ); } diff --git a/app/code/Magento/AdvancedSearch/README.md b/app/code/Magento/AdvancedSearch/README.md index 49cafc827d7c..bfb217b97cb9 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 8cdbaf781ed0..16a63d65d53c 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/AdvancedSearch/Test/Unit/Helper/DataTest.php b/app/code/Magento/AdvancedSearch/Test/Unit/Helper/DataTest.php new file mode 100644 index 000000000000..936d7c0928f0 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Test/Unit/Helper/DataTest.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AdvancedSearch\Test\Unit\Helper; + +use Magento\AdvancedSearch\Helper\Data; +use Magento\Framework\App\Helper\Context; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Magento\Framework\Search\EngineResolverInterface; + +/** + * @covers \Magento\AdvancedSearch\Helper\Data + */ +class DataTest extends TestCase +{ + + /** + * @var Data + */ + private $helper; + + /** + * @var Context|MockObject + */ + private $contextMock; + + /** + * @var EngineResolverInterface|MockObject + */ + private $engineResolverMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManager; + + /** + * @return void + */ + protected function setUp(): void + { + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->engineResolverMock = $this->getMockForAbstractClass(EngineResolverInterface::class); + + $this->engineResolverMock->expects($this->any()) + ->method('getCurrentSearchEngine') + ->willReturn(''); + + $this->objectManager = new ObjectManagerHelper($this); + $this->helper = $this->objectManager->getObject( + Data::class, + [ + 'context' => $this->contextMock, + 'engineResolver' => $this->engineResolverMock + ] + ); + } + + public function testIsClientOpenSearchV2() + { + $this->assertIsBool($this->helper->isClientOpenSearchV2()); + } +} diff --git a/app/code/Magento/Amqp/README.md b/app/code/Magento/Amqp/README.md index 6a47a072390a..e39dde060d43 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 a7f7d87b650e..cc1f6a71d77f 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 b992c84814a2..66423b421021 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 c5abff11e284..97f36e4d66b8 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 6090b5594a67..dd1d1e11cab7 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 9f07adb0223a..6e88124f6f22 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 0df1a6809cca..7b58b1468114 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 badad120fdcc..a0df3f4229a7 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 000000000000..61ee1ac90161 --- /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 000000000000..dc3c624a6a43 --- /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 000000000000..36b2459f6aa6 --- /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 000000000000..9647c29b8800 --- /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 000000000000..a4eead74a7c5 --- /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 000000000000..5088aa9f0d09 --- /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 000000000000..f83b96016a79 --- /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 000000000000..53131297717a --- /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 000000000000..a249cf27aec8 --- /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 000000000000..c42fd98111e4 --- /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 000000000000..76e21e849d25 --- /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 000000000000..38ad6b9d5716 --- /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 000000000000..c008ea290739 --- /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 000000000000..bc425d97437c --- /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 000000000000..4707bc88baa6 --- /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 000000000000..0fa759904266 --- /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 000000000000..62855ceead1f --- /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 000000000000..b30c8b7cebff --- /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 000000000000..cb235e328cbc --- /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 000000000000..fd94ddf4662f --- /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 0a71c130fb20..d046cbfdb25a 100644 --- a/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Notification/Dismiss.php +++ b/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Notification/Dismiss.php @@ -8,12 +8,13 @@ use Magento\AsynchronousOperations\Model\BulkNotificationManagement; use Magento\Backend\App\Action\Context; use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; /** * Class Bulk Notification Dismiss Controller */ -class Dismiss extends Action +class Dismiss extends Action implements HttpPostActionInterface { /** * @var BulkNotificationManagement @@ -43,7 +44,7 @@ protected function _isAllowed() } /** - * {@inheritdoc} + * @inheritdoc */ public function execute() { @@ -55,7 +56,7 @@ public function execute() $isAcknowledged = $this->notificationManagement->acknowledgeBulks($bulkUuids); /** @var \Magento\Framework\Controller\Result\Json $result */ - $result = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $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 6f908a4c5b21..e7bd6d99cb3e 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 47c317138ec6..58cc92e649eb 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 000000000000..92dd301608c1 --- /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 bd357e101328..71bded90a468 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 cc826d66211c..6984b7a3e03b 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 463989efdfa4..7a791132b83b 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 @@ -81,6 +81,11 @@ public function testExecute() ->with(ResultFactory::TYPE_JSON, []) ->willReturn($this->jsonResultMock); + $this->jsonResultMock->expects($this->once()) + ->method('setData') + ->with(['']) + ->willReturn($this->jsonResultMock); + $this->assertEquals($this->jsonResultMock, $this->model->execute()); } @@ -98,6 +103,11 @@ public function testExecuteSetsBadRequestResponseStatusIfBulkWasNotAcknowledgedC ->with(ResultFactory::TYPE_JSON, []) ->willReturn($this->jsonResultMock); + $this->jsonResultMock->expects($this->once()) + ->method('setData') + ->with(['']) + ->willReturn($this->jsonResultMock); + $this->notificationManagementMock->expects($this->once()) ->method('acknowledgeBulks') ->with($bulkUuids) 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 5365cb64c19c..2dbc6320808c 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 5f2fbd9ea8b1..3fbd93344792 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 44cc0a0ab775..8a2fc7f774a2 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 87dc0525eb1c..981e7ae98010 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 149c33f861b3..1ad01a96af20 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 000000000000..b29a8e7f9c53 --- /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 916903ffff36..bb5389dee62f 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 000000000000..6c057f81b9e3 --- /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 21420922ef59..bace3690a606 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 c2d0eaa1df97..f52cf7ebec2b 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 def5088e8932..52a8a5896abf 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 66c95e97ace0..b1cec93ed8f7 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 000000000000..0137284358bd --- /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 000000000000..337e9d2a2acf --- /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 000000000000..8ce9e8d72db1 --- /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 736623ccf47d..8624e06d6426 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/Unit/Driver/AwsS3FactoryTest.php b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3FactoryTest.php new file mode 100644 index 000000000000..48ff824528d2 --- /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 965ecaf6565d..0dd7e3fd7340 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 000000000000..f5c9b3138ea7 --- /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 73e6bc1ab9e8..4bdcd24d2b61 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 3d7154eb20f9..11cca3717ba2 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 74117fbd666c..66777b396843 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 a139d20191b5..c0c01c6201ce 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 0da7e4db9b98..b7928eb02745 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 22ca8a49c155..47922c18b20a 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 e65caf13f2ea..ec813472695d 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 2d1e5e977eaf..ab2ca43ef13f 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/Menu.php b/app/code/Magento/Backend/Model/Menu.php index 9506ac3dc36c..faa5f0cf8335 100644 --- a/app/code/Magento/Backend/Model/Menu.php +++ b/app/code/Magento/Backend/Model/Menu.php @@ -86,7 +86,7 @@ public function add(Item $item, $parentId = null, $index = null) $index = (int) $index; if (!isset($this[$index])) { $this->offsetSet($index, $item); - $this->_logger->info( + $this->_logger->debug( sprintf('Add of item with id %s was processed', $item->getId()) ); } else { @@ -151,7 +151,7 @@ public function remove($itemId) if ($item->getId() == $itemId) { unset($this[$key]); $result = true; - $this->_logger->info( + $this->_logger->debug( sprintf('Remove on item with id %s was processed', $item->getId()) ); break; diff --git a/app/code/Magento/Backend/Model/Menu/Director/Director.php b/app/code/Magento/Backend/Model/Menu/Director/Director.php index 7821a4dbae00..bb70eb331755 100644 --- a/app/code/Magento/Backend/Model/Menu/Director/Director.php +++ b/app/code/Magento/Backend/Model/Menu/Director/Director.php @@ -30,7 +30,7 @@ protected function _getCommand($data, $logger) { $command = $this->_commandFactory->create($data['type'], ['data' => $data]); if (isset($this->_messagePatterns[$data['type']])) { - $logger->info( + $logger->debug( sprintf($this->_messagePatterns[$data['type']], $command->getId()) ); } diff --git a/app/code/Magento/Backend/Model/Session/Quote.php b/app/code/Magento/Backend/Model/Session/Quote.php index ed0312874565..b3067d3c9885 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 10d5b7baec9a..74cf5a4fdd76 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 3d596d248c42..025255088d7e 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 cd6eca91e9e3..5382095673e8 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 440c73bc73a9..371c8dfbb8bf 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 5aaefc383f41..268ff07850f4 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 f7c5ae308c75..6bb832843d25 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 f24a7aaed3d2..ea7bc277231b 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 e0cbed316cf0..00b240fc19c8 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 88660b74cd6f..1b1f1deada58 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 b4f44ea9e0a6..1b80a1d897cc 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 000000000000..9407e6f9fefd --- /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 8ad10841ef9d..6b1f7e411e2f 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 eaf3fd240417..4babc8a266b8 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 f27d02c75194..a40e6f474c1c 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 8c3ebd96f502..61f8ee946194 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 73b14bdc1415..915b00e9189d 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 b19981f78df6..98c6c01053ff 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 e5b92f61230b..06a433e17ae6 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 45a49f58788f..5d4f1a4f3148 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 c4cbfcfaa12b..6ad97b44999b 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 d4ec0829604b..133be0bd74bb 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 664067a66d20..1afd62516774 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 22b45210fe6b..d9aa8c2850c6 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 b1b780190a69..d45171890e78 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 d69ceeba29d1..452517b46065 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 44c230e271a1..961e08b21efb 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 a6510c5d8271..b931d606a9da 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/Unit/Block/Widget/Grid/Column/Filter/DateTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DateTest.php index 575403824679..0a387b31bf8e 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 3296680f4337..4b63e34cbc87 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 049284072ae8..3d23f009ae29 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 463976b58212..1610ea9fde71 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/i18n/en_US.csv b/app/code/Magento/Backend/i18n/en_US.csv index b72db96d6a59..64ce31433b19 100644 --- a/app/code/Magento/Backend/i18n/en_US.csv +++ b/app/code/Magento/Backend/i18n/en_US.csv @@ -240,7 +240,10 @@ password,password "Reload Data","Reload Data" "Browse Files...","Browse Files..." Magento,Magento +"Mage-OS","Mage-OS" "Copyright © %1 Magento Commerce Inc. All rights reserved.","Copyright © %1 Magento Commerce Inc. All rights reserved." +"Thank you for choosing Mage-OS.","Thank you for choosing Mage-OS." +"Learn more about Mage-OS.","Learn more about Mage-OS." "ver. %1","ver. %1" "Magento Admin Panel","Magento Admin Panel" "Account Setting","Account Setting" diff --git a/app/code/Magento/Backend/view/adminhtml/layout/admin_login.xml b/app/code/Magento/Backend/view/adminhtml/layout/admin_login.xml index 9ddcb0f3b3bb..85e184df0d49 100644 --- a/app/code/Magento/Backend/view/adminhtml/layout/admin_login.xml +++ b/app/code/Magento/Backend/view/adminhtml/layout/admin_login.xml @@ -24,7 +24,7 @@ <move element="logo" destination="login.header" before="-"/> <referenceBlock name="logo"> <arguments> - <argument name="logo_image_src" xsi:type="string">images/magento-logo.svg</argument> + <argument name="logo_image_src" xsi:type="string">images/mage-os-logo.svg</argument> </arguments> </referenceBlock> diff --git a/app/code/Magento/Backend/view/adminhtml/layout/default.xml b/app/code/Magento/Backend/view/adminhtml/layout/default.xml index 1c28d5fc5935..8feb4d5163cb 100644 --- a/app/code/Magento/Backend/view/adminhtml/layout/default.xml +++ b/app/code/Magento/Backend/view/adminhtml/layout/default.xml @@ -26,7 +26,7 @@ <arguments> <argument name="show_part" xsi:type="string">logo</argument> <argument name="edition" translate="true" xsi:type="string">Community Edition</argument> - <argument name="logo_image_src" xsi:type="string">images/magento-icon.svg</argument> + <argument name="logo_image_src" xsi:type="string">images/mage-os-icon.svg</argument> </arguments> </block> <block class="Magento\Backend\Block\GlobalSearch" name="global.search" as="search" after="logo" aclResource="Magento_Backend::global_search"/> @@ -63,12 +63,12 @@ <block class="Magento\Backend\Block\Page\Footer" name="version" as="version" /> <block class="Magento\Framework\View\Element\Template" name="privacyPolicy" as="privacyPolicy" template="Magento_Backend::page/privacyPolicy.phtml"> <arguments> - <argument name="privacypolicy_url" xsi:type="string">https://www.adobe.com/privacy/policy.html</argument> + <argument name="privacypolicy_url" xsi:type="string">https://mage-os.org/privacy-policy</argument> </arguments> </block> <block class="Magento\Framework\View\Element\Template" name="report" as="report" template="Magento_Backend::page/report.phtml"> <arguments> - <argument name="bugreport_url" xsi:type="string">https://github.com/magento/magento2/issues</argument> + <argument name="bugreport_url" xsi:type="string">https://github.com/mage-os/mageos-magento2/issues</argument> </arguments> </block> </container> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/copyright.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/copyright.phtml index e3a5c84ea452..293fe82b7f09 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/page/copyright.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/page/copyright.phtml @@ -3,6 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +use Magento\Backend\Block\Page\Copyright; +use Magento\Framework\Escaper; + +/** @var Escaper $escaper */ +/** @var Copyright $block */ ?> -<a class="link-copyright" href="http://magento.com" target="_blank" title="<?= $block->escapeHtmlAttr(__('Magento')) ?>"></a> -<?= $block->escapeHtml(__('Copyright © %1 Magento Commerce Inc. All rights reserved.', date('Y'))) ?> +<?= $escaper->escapeHtml(__('Thank you for choosing Mage-OS.')); ?> +<a class="link-copyright" + href="https://mage-os.org/" + target="_blank" + title="<?= $escaper->escapeHtmlAttr(__('Mage-OS')); ?>"> + <?= $escaper->escapeHtml(__('Learn more about Mage-OS.')); ?> +</a> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/footer.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/footer.phtml index 3f21dcda9a54..7b1f14d28a50 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/page/footer.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/page/footer.phtml @@ -3,8 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +use Magento\Backend\Block\Page\Footer; +use Magento\Framework\Escaper; + +/** @var Escaper $escaper */ +/** @var Footer $block */ ?> <p class="magento-version"> - <strong><?= $block->escapeHtml(__('Magento')) ?></strong> - <?= $block->escapeHtml(__('ver. %1', $block->getMagentoVersion())) ?> + <strong><?= $escaper->escapeHtml(__('Mage-OS')); ?></strong> + <?= $escaper->escapeHtml(__('ver. %1', $block->getMagentoVersion())); ?> </p> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml index 89f144664003..f5d9172a51c4 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml @@ -3,62 +3,74 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -/** @var $block \Magento\Backend\Block\Page\Header */ +use Magento\Backend\Block\Page\Header; +use Magento\Framework\Escaper; + +/** @var Escaper $escaper */ +/** @var Header $block */ $part = $block->getShowPart(); ?> -<?php if ($part === 'logo') : ?> - <?php $edition = $block->hasEdition() ? 'data-edition="' . $block->escapeHtml($block->getEdition()) . '"' : ''; ?> - <?php $logoSrc = ($block->hasLogoImageSrc()) ? $block->escapeHtml($block->getLogoImageSrc()) : 'images/magento-logo.svg' ?> - <a - href="<?= $block->escapeUrl($block->getHomeLink()) ?>" - <?= /* @noEscape */ $edition ?> - class="logo"> - <img class="logo-img" src="<?= /* @noEscape */ $block->getViewFileUrl($logoSrc) ?>" - alt="<?= $block->escapeHtml(__('Magento Admin Panel')) ?>" title="<?= $block->escapeHtml(__('Magento Admin Panel')) ?>"/> - </a> -<?php elseif ($part === 'user') : ?> - <div class="admin-user admin__action-dropdown-wrap"> - <a - href="<?= /* @noEscape */ $block->getUrl('adminhtml/system_account/index') ?>" - class="admin__action-dropdown" - title="<?= $block->escapeHtml(__('My Account')) ?>" - data-mage-init='{"dropdown":{}}' - data-toggle="dropdown"> - <span class="admin__action-dropdown-text"> - <span class="admin-user-account-text"><?= $block->escapeHtml($block->getUser()->getUserName()) ?></span> +<?php if ($part === 'logo'): ?> + <?php $edition = $block->hasEdition() + ? 'data-edition="' . $escaper->escapeHtml($block->getEdition()) . '"' + : ''; ?> + <?php $logoSrc = $block->hasLogoImageSrc() + ? $escaper->escapeHtml($block->getLogoImageSrc()) + : 'images/mage-os-logo.svg'; ?> + <a href="<?= $escaper->escapeUrl($block->getHomeLink()); ?>" + <?= /* @noEscape */ $edition; ?> + class="logo"> + <img class="logo-img" + src="<?= $escaper->escapeUrl($block->getViewFileUrl($logoSrc)); ?>" + alt="<?= $escaper->escapeHtml(__('Magento Admin Panel')); ?>" + title="<?= $escaper->escapeHtml(__('Magento Admin Panel')); ?>"/> + </a> +<?php elseif ($part === 'user'): ?> + <div class="admin-user admin__action-dropdown-wrap"> + <a href="<?= $escaper->escapeUrl($block->getUrl('adminhtml/system_account/index')); ?>" + class="admin__action-dropdown" + title="<?= $escaper->escapeHtmlAttr(__('My Account')); ?>" + data-mage-init='{"dropdown":{}}' + data-toggle="dropdown"> + <span class="admin__action-dropdown-text"> + <span class="admin-user-account-text"> + <?= $escaper->escapeHtml($block->getUser()->getUserName()); ?> </span> - </a> - <ul class="admin__action-dropdown-menu"> - <?php if ($block->getAuthorization()->isAllowed('Magento_Backend::myaccount')) : ?> - <li> - <a - href="<?= /* @noEscape */ $block->getUrl('adminhtml/system_account/index') ?>" - <?= /* @noEscape */ $block->getUiId('user', 'account', 'settings') ?> - title="<?= $block->escapeHtml(__('Account Setting')) ?>"> - <?= $block->escapeHtml(__('Account Setting')) ?> (<span class="admin-user-name"><?= $block->escapeHtml($block->getUser()->getUserName()) ?></span>) - </a> - </li> - <?php endif; ?> - <li> - <a - href="<?= /* @noEscape */ $block->getBaseUrl() ?>" - title="<?= $block->escapeHtml(__('Customer View')) ?>" - target="_blank" class="store-front"> - <?= $block->escapeHtml(__('Customer View')) ?> - </a> - </li> + </span> + </a> + <ul class="admin__action-dropdown-menu"> + <?php if ($block->getAuthorization()->isAllowed('Magento_Backend::myaccount')): ?> <li> - <a - href="<?= /* @noEscape */ $block->getLogoutLink() ?>" - class="account-signout" - title="<?= $block->escapeHtml(__('Sign Out')) ?>"> - <?= $block->escapeHtml(__('Sign Out')) ?> + <a href="<?= $escaper->escapeUrl($block->getUrl('adminhtml/system_account/index')); ?>" + <?= /* @noEscape */ $block->getUiId('user', 'account', 'settings'); ?> + title="<?= $escaper->escapeHtml(__('Account Setting')); ?>"> + <?= $escaper->escapeHtml(__('Account Setting')); ?> + (<span class="admin-user-name"> + <?= $escaper->escapeHtml($block->getUser()->getUserName()); ?> + </span>) </a> </li> - </ul> - </div> + <?php endif; ?> -<?php elseif ($part === 'other') : ?> - <?= $block->getChildHtml() ?> + <li> + <a href="<?= $escaper->escapeUrl($block->getBaseUrl()); ?>" + title="<?= $escaper->escapeHtml(__('Customer View')); ?>" + target="_blank" + class="store-front"> + <?= $escaper->escapeHtml(__('Customer View')); ?> + </a> + </li> + <li> + <a href="<?= $escaper->escapeUrl($block->getLogoutLink()); ?>" + class="account-signout" + title="<?= $escaper->escapeHtml(__('Sign Out')); ?>"> + <?= $escaper->escapeHtml(__('Sign Out')); ?> + </a> + </li> + </ul> + </div> +<?php elseif ($part === 'other'): ?> + <?= $block->getChildHtml(); ?> <?php endif; ?> 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 5702b0878dec..8737b5a50cdb 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 864e5f4b3772..252ca89ae411 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 75576095e3cd..963b9bdd07d8 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 c38a7b3005e2..cf39406d54ae 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 4d5f0941dd45..5a2445bfa7ea 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 4ae20c711327..b9c6b67cf1bc 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 000000000000..921f871da726 --- /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 8f89910558c9..8c4f19193e22 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 d657da6cbfe3..d17e8e0b16a1 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 9bc056a3e9a8..3569a026144b 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 0fe0f7d97ea0..2776f11db33f 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 2f6708a17639..b7260dd49b20 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 1914d5b5146c..c55500b8461f 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 42c6930469ac..ff5b902c37e7 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 e536b8961ebb..412783565486 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 d3f1c2f1c999..424330a1671e 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 d542458c365a..511dbd092bcf 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 c322a4b26241..1562620fe03a 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 c04b754e8cbc..0a098b1fd6fe 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 b3c3e74e1fa6..1730c3b62d3b 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 6808081506dd..173e8f257b06 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 000000000000..bfea7dc1295c --- /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 45018406277f..14578aedd1dc 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 303c33b571d3..04f4305bdf77 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 000000000000..3475eb5cd3d8 --- /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 bde963321208..517f49ab9af2 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 c4cdb0aaf92c..640c59cdb198 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 232fdf6a4fda..4054f74cccb1 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 3051394eaf51..5e38edcb3760 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 d09215bff7b0..ac78051d95ae 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 e4951cc31173..4ac7bdd798e3 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 9bda194df4b0..5028c35eea00 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 000000000000..add5ee7b1229 --- /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 952ae69d887d..5cf4903a67ad 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 84b0dc144987..e3a3ab40b5bf 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 bfeb5c6bcb4b..d5f8a6b4e820 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 7038ad90b81b..67f2e4a0cbcc 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 e5f557dd22de..67844e8759bf 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 8b78ac7b5fe6..295243f8a81d 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 90d47fd10502..0fb00c89cc12 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 2fde274dc528..03a09af81967 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 9722835b201c..3ee0bc40cd76 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 000000000000..25c94f015a10 --- /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 5936948d0a8c..37b988dbcb23 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 a2f1bb068ee4..10330248d775 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 cbba28485969..ed0ba3867418 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 d7d053c3e1f5..9538542c75de 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 65aad618d5eb..d23e57240920 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 a41e1f369b70..8cebf1900cda 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 3edca9a5bb7f..f37224cd76ce 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 000000000000..a6fa71c727ee --- /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 8f556734ab5e..9bb6871c47a5 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 7bcd4d0899ed..800849c37b68 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 9d24d4f8d38b..2a097d105c27 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 2f7dd14d1d71..065f36b20fd9 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 467fd965e328..084047b635bb 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 fcf2b39e9701..601d4ff81951 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 79c7d113477f..123e494ed944 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 b5b0fa3187b0..8240e87ed76a 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 77a43721d4e6..14895e1aa617 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 a7d41069a4d9..3fc94bf4d214 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 5c23360e74d7..35ac017d39c8 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 643e71626e62..2a76237ae6f5 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 482c8ed50367..7d40b5a41706 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 daa3351073e9..e101130598e8 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 fe5bbf5bcd3e..4d7af01c44e3 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 42b3f16ded35..8492f2453f30 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 cab4b09bbd64..27b834a3a043 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 b8bda30faa44..0f62188b6b5e 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 eadf7667b010..9e4a57cd38ef 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 2e85f8305bba..70e4b000cbf3 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 30397d847355..5d02081c0816 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 c56e09562d49..fc577bf4dd8e 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 1b33bb08b1b0..55d9439a544e 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 381f265f6d8b..cbd71fac8aec 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 ddea67a8a3e0..b7f096f3712e 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 eb047822cd23..348833f0584e 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 000000000000..b910186b66e5 --- /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 000000000000..815e25b1cad6 --- /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 5758a782d3b5..e3f844f90a51 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 753d6f965507..f993c3dfaf23 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 f4b81e9ba957..b6ad937eac4a 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 792590b14b4f..2a9da5a5243c 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 378c59048cde..33ab0abfd6c1 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 37e743c0dc04..baaa2c11e159 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 63ed6c669d25..4e121a7b41b2 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 7883cc4faf00..d3db64835e37 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 55bb27d317c1..8344dbce0850 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 640f9040e588..28e75bf25c7a 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 2261e5dc42d7..279f3a8bb7e1 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 dc1beea6609e..c8c47b2e5400 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 918e6014dbb9..2bec268ab2e8 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 63f94401a3c1..c30f09648ffc 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 853275a0af6a..6044b5401aef 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 61545268ef63..6d46122b2dea 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 9c334fea8d80..9975ef4be0df 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 31e8ff339112..ed62af198ea1 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 3fa758effc18..e9ce22a0913e 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 9ab7df0f5dc7..722dd2b90666 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 b486d95ac3e4..de57428d713b 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/Unit/Helper/Catalog/Product/ConfigurationTest.php b/app/code/Magento/Bundle/Test/Unit/Helper/Catalog/Product/ConfigurationTest.php index e5edc5fb5396..67fd2eb9d7b8 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 000000000000..1b8fd65455f3 --- /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 000000000000..3eeac7a324c0 --- /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 68310d1d2bb4..a222b3c3eff3 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 4cdb3913f96e..63ae344e888c 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 000000000000..23a5f980a83a --- /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 000000000000..a2ac9b109de2 --- /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 24aeffc1e33c..f957e72c1484 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 0adb1f5b9730..3e2e38ea5144 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 9eb0e7aa8946..2cf0c201f120 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 5f1ffc3c2682..827082dc7744 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 000000000000..a2170aa30f8d --- /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 4e2f17fa46d4..7b1c254eae6f 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 f173bb26fcc3..4f3069dee65b 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 c5c4a491234e..7601224056be 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 28a236d1fb35..29f2fd449410 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 26264cc2cc87..a77a6654247d 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 fe01f23bb451..15c69f4235f6 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 706b28049470..5f3e219866ba 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 184f7177a995..2d842b87faef 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 b67bd69ecf92..028772f5b288 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 000000000000..004b98164641 --- /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 3d479692f719..9a4e5b94c40a 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 2fa0ce6def9d..5f1fe2c580a7 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 dfdf4e904a47..8da272dce33f 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 2f4cd2db8cea..caca08d3d4a9 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 6ebdddfbedc5..f75b731ea83e 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 d94413e8c2bb..79fec1cae451 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 af7fb0af4685..4d8496def477 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 89b095279877..b211ad2a9cf3 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 000000000000..8796f16b2776 --- /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 6cca6ffec03e..f12b0435e71b 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 35979fb2b489..d4119e03e1d9 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 54aa36d1ca26..1aabfb8a0c42 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 132d5628b400..6ebb0fda0308 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 1d5dc170daef..5234c1f64600 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 40d66bcba82d..d46d43a63339 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 3a55535e33ae..68fc597f41ce 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 f2e0ca5e433c..fd15cf9e820d 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 89f937fa4559..ba7667436f05 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 95f6ebfdb636..5de01f29c40a 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 428068baefeb..871d427e39a0 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 912e637dc534..4ab4ec7f055f 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 54db9114a2a0..aa68470a496b 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 8fdd6de99ad1..5d45a29d0124 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 550caf585b8f..3fa01d7f7b82 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 acffce3ca0b8..c1ab3d7bac2d 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 b0f00d0f2b04..0b871093d402 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 e0836a0d7cb2..c81344a99768 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 20bd1b379bee..97aa2c49e992 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 9c83d4aea61c..dba669bc5ca4 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 efc692a62a74..e3d8c1a045bc 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 d927378012f1..34b3814411bf 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 cd6c5021f0cc..2ede6c15eb76 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 8f1d1dcf7eed..a2dc05cea07d 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 48753bfd6efb..30568258f1c4 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 f8ea447879e9..e55e9f5c239a 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 00a4b605fba4..f6f56115012a 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 98fcc03e6511..c51889e100d4 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 7919708aaa8a..861ff83a4070 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 a0ca53dce4f5..72e06548a50f 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 514b224c5332..68a3fcf59813 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 560be70a789f..8ce74b0be74a 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 cb9f9a052506..5ee3f37d8eaf 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 5e03028c9a1f..40a570e51667 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 66e04ef03f77..d1b90dd62ddc 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 a9b10d97ec00..00de4ddfcab4 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 3b9036c1fbbc..466809c091a5 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 1b0ab706e7d4..d92dea0b2dba 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 26ffc6e0df3d..06826ec0b645 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 cb0a739b56e4..d914c521088f 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 93c2dcc76263..bda48b098ae9 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 f69e58985bfc..b34d5a49c9d0 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 e29ab26065dc..94cd8fad2a99 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 b06edc43cd71..468e50b2b070 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 674c99001b01..558b833f0794 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 da0211ad2055..a91f33ba7434 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 0384c9cd9acc..d2e4745aa79f 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 28786e2429da..f9bdf3cba046 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 4491ad1ee99b..d6c3037d5bf6 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 81ab67bdf26d..7950896f8ef5 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 0b1ef98c386c..ea14dbc1ce62 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 97b57317851f..5c0068ac06ed 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 196eb313bc62..5a47e53440c9 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 615696aea396..cfc93156a5df 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 ae42acf4b196..c09ba2df1aac 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 1bc7388f4e9e..92b4b1c8dc34 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 93eaa23b89f1..89f44ef73e09 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 7082fa4747fd..4e85853cd33b 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 c6feb049e1a1..dbbfb8490aee 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 48d79ec54c75..6af28494efc2 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 49d8336dddb2..4119335d37b4 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 50700e672237..fc06f6228d04 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 e69ab504880e..219467033ecd 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 fb94e82f0007..65f2db475e02 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 86a7d1fb6193..08d0441e919f 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 a4224aeafe7e..b6ca16f1ac02 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 0191c0239fc2..54d5ee203e06 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 68aeabfc70d3..4623c095e6d8 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 26db7e704d2c..634133d3feae 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 2ff5b14fcb9e..7bd1c32daa50 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 c0a13aa8b934..020176738160 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 1105960e36d8..85a69a9e69be 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 edee9aef508d..9d3dee8994c4 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 c5e5e0ecac4c..0105a224b2c0 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 000000000000..b445c830834b --- /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 ecdb3b2829b9..ad2559534c36 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 000000000000..bc2cff2a4010 --- /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 2297f39829aa..99c2513f5187 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 225f1bb3d10e..6a894278a049 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 000000000000..609d02e33757 --- /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 f23ce0faf708..6bbb0951ec19 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 45ba1de85260..872a3e31eb57 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 fb25b6703b73..eee62527094f 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 c3a88a505c51..d8af4a965630 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 daf8790c419f..eb493671e613 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 5cf4ac1e6424..c586563759b5 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 c71225b4fc67..5446a89becc5 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 d6bc3ed1d86d..30b0c4315bc3 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 61f2c1838a1c..1c2a315490f3 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 35828fc8ec11..765065bb5fde 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 351e3314c9fb..9df0a3a9b383 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 a121648b7acb..9c77f144d7c7 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 c950b49348dc..1256ab1caa93 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 94b3ee03d5d9..000000000000 --- 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 79636c55c0f5..5b3360526654 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 4ae58c98f297..000000000000 --- 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 ff29a5afa7ed..46d9674f4061 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 76f566a36476..42ce2fcae007 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 4259504b8f0f..c1525d16275d 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 e024f0d30f1d..5486d4ad8d29 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 ce097dd95d77..392032a68bc1 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 bca919e70036..cac549e0a17c 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 43762306b2b6..f7c02cc93bf9 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 000000000000..0df9da31f80a --- /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 c7422f72a5c2..8f6386b8341f 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 af2dccb96f93..2658e7982021 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 ce2fe19cf1ae..181eaed824bf 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 ef3abddf91e6..5684ea79a672 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 a6a11fb803bd..8d3916b535ff 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 a5e573caa381..6945bf526cff 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 5fba207bdeb0..6033e7deaeac 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 5b84d1d2e7ec..ef95c1effe0a 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 846784718d02..baa69c473201 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 000000000000..7912e876e88d --- /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 1f68eb2b832d..f4efdcf5038c 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 000000000000..ffa95ba8f8f6 --- /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 000000000000..303ddd723d6c --- /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 c665acd09cd0..f856bff65a1b 100644 --- a/app/code/Magento/Catalog/Test/Fixture/Product.php +++ b/app/code/Magento/Catalog/Test/Fixture/Product.php @@ -10,7 +10,10 @@ use Magento\Catalog\Api\Data\ProductCustomOptionInterface; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Config\Source\ProductPriceOptionsInterface; use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Option as CustomOption; +use Magento\Catalog\Model\Product\Option\Value as CustomOptionValue; use Magento\Catalog\Model\Product\Type; use Magento\Catalog\Model\Product\Visibility; use Magento\Framework\DataObject; @@ -117,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()]); } /** @@ -198,21 +197,57 @@ private function prepareOptions(array $data): array { $options = []; $default = [ - 'product_sku' => $data['sku'], - 'title' => 'customoption%order%%uniqid%', - 'type' => ProductCustomOptionInterface::OPTION_TYPE_FIELD, - 'is_require' => true, - 'price' => 10.0, - 'price_type' => 'fixed', - 'sku' => 'customoption%order%%uniqid%', - 'max_characters' => null, + CustomOption::KEY_PRODUCT_SKU => $data['sku'], + CustomOption::KEY_TITLE => 'customoption%order%%uniqid%', + CustomOption::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD, + CustomOption::KEY_IS_REQUIRE => true, + CustomOption::KEY_PRICE => 10.0, + CustomOption::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + CustomOption::KEY_SKU => 'customoption%order%%uniqid%', + CustomOption::KEY_MAX_CHARACTERS => null, + CustomOption::KEY_SORT_ORDER => 1, 'values' => null, ]; + $defaultValue = [ + CustomOptionValue::KEY_TITLE => 'customoption%order%_%valueorder%%uniqid%', + CustomOptionValue::KEY_PRICE => 1, + CustomOptionValue::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + CustomOptionValue::KEY_SKU => 'customoption%order%_%valueorder%%uniqid%', + CustomOptionValue::KEY_SORT_ORDER => 1, + ]; $sortOrder = 1; foreach ($data['options'] as $item) { - $option = $item + ['sort_order' => $sortOrder++] + $default; - $option['title'] = strtr($option['title'], ['%order%' => $option['sort_order']]); - $option['sku'] = strtr($option['sku'], ['%order%' => $option['sort_order']]); + $option = $item + [CustomOption::KEY_SORT_ORDER => $sortOrder++] + $default; + $option[CustomOption::KEY_TITLE] = strtr( + $option[CustomOption::KEY_TITLE], + ['%order%' => $option[CustomOption::KEY_SORT_ORDER]] + ); + $option[CustomOption::KEY_SKU] = strtr( + $option[CustomOption::KEY_SKU], + ['%order%' => $option[CustomOption::KEY_SORT_ORDER]] + ); + if (isset($item['values'])) { + $valueSortOrder = 1; + $option['values'] = []; + foreach ($item['values'] as $value) { + $value += [CustomOptionValue::KEY_SORT_ORDER => $valueSortOrder++] + $defaultValue; + $value[CustomOptionValue::KEY_TITLE] = strtr( + $value[CustomOptionValue::KEY_TITLE], + [ + '%order%' => $option[CustomOption::KEY_SORT_ORDER], + '%valueorder%' => $value[CustomOptionValue::KEY_SORT_ORDER] + ] + ); + $value[CustomOptionValue::KEY_SKU] = strtr( + $value[CustomOptionValue::KEY_SKU], + [ + '%order%' => $option[CustomOption::KEY_SORT_ORDER], + '%valueorder%' => $value[CustomOptionValue::KEY_SORT_ORDER] + ] + ); + $option['values'][] = $value; + } + } $options[] = $option; } diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddCrossSellProductBySkuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddCrossSellProductBySkuActionGroup.xml index 3c6e08bdec55..36a7f39d68ae 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 e915f59100ac..2fe56d9679f9 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 12602615db8e..b52188b05486 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 ca82882b141c..155cc9a6156f 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 000000000000..ede8a2b3be16 --- /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 ec3d26e8a3f3..c8aee3a9115d 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 f27a08eb3e0b..f1b20569700b 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 000000000000..2907f88446ec --- /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 000000000000..020a42680810 --- /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 000000000000..4cee97298515 --- /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 000000000000..6db711e24aac --- /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 000000000000..9b12b1b347ac --- /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 a84e92fcbb0f..337ec59b60f7 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 000000000000..38865284a101 --- /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 000000000000..1e60b5354b36 --- /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 635e36c45851..7ca4d177ae4f 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 ee3a5067449d..c9bd8247c666 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 000000000000..6bd7bf90491e --- /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 e5b6efbd6373..57d5103a87a7 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 e1072001b56e..ebde9601149e 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 93bc62f3d7d0..6791c9ad7787 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 c178c5ed0fd2..0436609c7e73 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 c34d7e1a4126..9792a6165ad7 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 c22677f0e0f5..8b1a25adb267 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 aa8657304427..021057e06e7e 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 e4b33ac79555..8e2877b47b64 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 0a3c67bc00d5..acfd9bdac2fe 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 05391d9babce..57824d73f4d7 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 0de3e6de1dee..58a8a77781f7 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 387e252ae93d..acc88d000177 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 526ac700a0b5..744b279e4c77 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 61e6a345b9ba..de1c010797b6 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 26a5452ee018..6edef36fd98f 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 52a377ad264c..3aee31f3b558 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 7b9e70c59dbc..00d525c07ef8 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 6ea8102a035d..47bfa67168e9 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 7be02126e3a0..5db689b8b5cb 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 3757ba2f5e21..13ffbc45644d 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 38d8b572ac62..34603cbc6b3c 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 f5dec88789bf..cffeb9006d44 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 b677fae5e58e..b5068e5b0b37 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 35f17f1dcb32..af67fc0e9b82 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 bed5297041dd..ef4b1879dde5 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 d713660d7ee6..412fa19a295a 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 e4cf255a03e0..b62aa752449a 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 0441c78cf223..fbc43eb579cf 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 381fbdec1214..4cef981b357f 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 5786eabf9c84..4f2dd66e4d4f 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 471880b5f6ea..c53234ab3f56 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 ee275ce4514e..69aeaec9ddc5 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 cfaf0c4b88ad..81dc9e84f30f 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 85a125090914..5ea53da9b78b 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 a3d50a9c361b..78be5ea65119 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 f5cf4cd3f241..16a2b4f54675 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 8d59e475ca10..6d3eddbfed5b 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 1dec1073f56e..40b6ddeaff50 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 c682c7ab4001..0a02162903e9 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 68e604027724..21d7e4602522 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 e7d4241500bf..86be4101ccae 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 806366a7ad57..3a07e4a8c347 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 f2413a152339..648e5251657e 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 ca0616213c59..37ff43deeecf 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 75f805bb99e0..3bf36ca9e948 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 97992c35b731..a30209128e7d 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 88d24540b11f..dac214f03d45 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 ce4cb250796b..979ffe305a62 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 92a3b298aa6b..b460c8125c22 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 c15cedadb446..9892b13b3698 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 db789d3512ac..9ee44edaa0de 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 f956c7331942..45506ee1cc8a 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 b23ce827d5d6..2cfb992617d3 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 3fdd278a6bac..fe7426bad856 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 2cdec1405e9f..26faf7bb42a4 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 000000000000..65ac3cc24ae3 --- /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 a865cbfdef22..384e3d9362c7 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 52ff9baee243..954c0b0d6d81 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 c42569385c59..bf1835c03fb7 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 7191f1971b31..6406c914f40f 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 19064458ae2a..472d2a826a8f 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 f2fcc7a3e890..3408ba982739 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 ab0ae5721a7a..3ab36faf8d01 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 852353300d09..4190adbf6b3b 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 83404391abca..fe722f73e850 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 6f183a44d827..2958a76f4836 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 000000000000..3d640eb8e1be --- /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 adb9d9bd824f..68c2e80e5dd9 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 10ab616ab6c7..fd645a7380cd 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 a711228e659b..9812f6466081 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 6d7d56861b73..440d0b5ee048 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 f60312f19a7e..d8775fd30700 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 31ad92afb9d4..135d4a7ac3c3 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 3f51fa229621..877a084aa24c 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 6f97cc7abe71..ea5209dce6f3 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 5931193dbe7c..30e1924b0319 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 5c00028ee69b..d14f1c981537 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 47c7f86067cf..3460d29e2b36 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 f5b0ebfc40eb..56f43a54cea9 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 c2557b44bc68..6b578425d394 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 45b776a6c871..825a57d27311 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 52cac23574b5..d8af1d3194d1 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 e5251b5fee40..8a2abf5df5eb 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 fc5fa60f754c..6445fbf31e63 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 90730a6516d3..26ce90a8cacd 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 da87880477de..2e50911f7655 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 7a087f02a3ff..290a1991c825 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 686f8aa865c2..1fdc7e156382 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 d129ad3a04d0..160e7e8908d9 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 c18f1c69f87f..7ca613ec0011 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 e61684b91c08..580f30e3548a 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 7df525f5f9f2..e7df46287282 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 f7f68b9f85c0..2f724aab7877 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 819835dead30..1bfe63748bf0 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 a18754f0ecb1..501a6dbb1e24 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 000000000000..29cc614edeb1 --- /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 4b40f04f098e..d3174a11d634 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 4ef9e2ec1fc6..e65e459cd97b 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 b1f18a770ea0..e7c53a72bb1c 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 d74b15b01ea3..31e0022f6f52 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 494ff1008e6e..a4cab3cad7b5 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 68b9c6b32c98..0ad6d399bdb7 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 18fb840202f4..f181092030c2 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 4c02c57dae53..23f3ff0566ee 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 71665e4064d5..a250353dd680 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 19fd3e2ad722..eb2dcd5afdcf 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 81d897d4836a..faed5ea16810 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 7aeb1a139795..d71247647caa 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 b82c6ba13550..3c01e43e85d4 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 e26a42006b0a..a6d1ebaa60c0 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 d4c7e20223a6..ed9104bf0859 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 841b08e70fb4..f6db6e27f21f 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 abbc541fbbcf..3ab09e3e9442 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 f43048f00e6b..e71289b59eaa 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 6712bf90c470..1a829649dc53 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 000000000000..d363229d3f43 --- /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 ae92e997e0aa..851c04bb5795 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 92b190efc621..8f6ce45bfd53 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 900c40dec14d..6924e4bbf213 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 2034ea8ec821..3038f1fe468f 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 b7e037b323ee..bbb6d73d2321 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 a6cd3c8b52b2..0b2d102eaab9 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 cb2e6b8e483a..efbc198b05a8 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 744dbcc32e7f..6d88ea477a88 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 db932e5d4751..11f97edf5019 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 83e9a70ad285..3f180ed0369d 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 2b1ba0894ecd..ce56fa18dee2 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 9f757ff72d06..0cd49c6a76ac 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 f10288bea36d..997c66d19098 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 82a9a610e32c..492310d457bf 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 ab1bbc39e393..8b725a8ddffb 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 367d4e0ec724..f92085d4d3c2 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 92fdc02d225a..ea899cf917c9 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 00c466e9aebe..11cab1532b82 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 264d35844a58..844881329c4e 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 eb33a1c7eecd..a6a1d24502f8 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 c8aba75838f5..81d05fefcdda 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 9c55e70c3c66..ab7b0ffba5a0 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 12ec57c0ef1a..a7679e7082b5 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 014104380bf5..736e676e0861 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 44a7dc4102e4..e32e0aeb4b4c 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 96a8f711ea56..256768848efb 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 efd2a54fc513..5de1be25fee7 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 14636d8b8ae3..1cf398d852c5 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 9eba952c1a3b..18d2b8bed579 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 f26e140ebdb2..97fb21c6737f 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 203ed2c530fb..ed1209144813 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 4f3feba01a92..a47080d63c21 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 f3981e7b8f76..d3b8113891d0 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 85fec54de2f0..3509234568d1 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 ef44d0b418b4..f7a073a16336 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 d677eda5b092..7ac6c3e0ffe9 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 449d20139320..6268bc4e65ca 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 30c1b9296553..d3008c2fbb39 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 bfa80c2e24b4..3d6a246b27b4 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 13f10185514e..11bcafc53070 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 410e945cea7e..a3f95eaeba5d 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 99fe4dd0c135..33a6a873d444 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 521256cf57dd..5720723ae446 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 4a544b60f15b..3854d34f5695 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 c5b475f616b7..d77db0f05cee 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 8033a2dffec7..aced4c878674 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 de3e2a9b9dad..2fdf988c48d6 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 f647492775b2..8ece4af9eaab 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 49add85f7680..041f690e627c 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 92818c846fcf..3a0b78a0fa81 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 000000000000..c4df428371db --- /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 6e0ad56f0d5d..35e34fd8cb2e 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 b2bbc3e016f5..c2a2420576d1 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 c3e939b4155c..6d5b41169b5c 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 000000000000..2877313e744c --- /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 17dac7600ef9..f342231f98f3 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 eff423989cd0..bc1b0ee818fd 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 000000000000..54c6ffa904bb --- /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 38ba4f4331c1..7d590b74f5a7 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 402e57898fe5..9fe3df202617 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 96a6dc7b70a7..17a8285ca5a4 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 a8eddeb8b613..55dcb87390b4 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 000000000000..554f8e2448b8 --- /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 7989de271b3a..bad2b25d89e6 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 bb6098f55cf9..4e63348610ed 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 bb7aca5ed770..94e314d0cbdf 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 ea50a17b47b4..360f3aa14a7a 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 f8c3857fb544..e88d7805a94f 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 4389bf4bd638..d166d22b804d 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 c04212a220f4..ce7153d20b73 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 051495b25701..89a7beda9e2f 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 2c7e26d4084b..41295987a259 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 3c4dd6078561..c880e232b8fc 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 8b2d447d297d..3d968114f4ac 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 0fd564d86f03..327cf03d957e 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 2b4840bd3619..90b3bc553296 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 ad14bc274a52..4fabff148552 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 359b560b1829..71c82f654310 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 2e72bb734fe0..c276b9ac2dbd 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 669e5cd040c5..511cb4af97d7 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 fa9aea768320..20da19b1a42f 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 4431991fdbb7..4ced0bff2a5b 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 01feac998060..302b90fb4bcd 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 214ca0e9b857..bea04c1714de 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 b436601356b3..44ccd7aa3ea6 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 607ebd1a626a..cab3f33e516b 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 27b65c53b835..a3050e015a93 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 b0c14bcb79e1..cb6a052a934b 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 71cb86e765fd..7c066343ee7f 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 6ff9e1b45359..07dfb8b52e36 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 55ef2a944f2f..56410c87a827 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 3d010eb96392..2b8a69a01d08 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 445f8b1c7372..260daaa09742 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 579aa1e65042..e66a4df7f770 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 4409e635e6ea..8fa2ec305a50 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 58e6d515a2e0..6168653b7dc7 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 c79567d7f674..b59531810d59 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 21873bc10acb..9f67560b014c 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 46c668dcf5ab..07744cb95791 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 b989450ea228..f921187cda63 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 703abf09c801..b6d31b319ab9 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 30771fcfd947..3d912906b4cb 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 5115399db9e3..a85dbe696bad 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 cacf4f3f4c9f..8a8ef7fdf6d7 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 6ca81ee49473..7754bfe14e80 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 dc6409043f67..3d892c2b1621 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 64da7e8599d0..e23f3472d8dc 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 12056962bac2..972616cad853 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 68a69644d3d7..cbedba0127eb 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 f6cfb58bf71d..a2db74321df2 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 132e82d49085..cbc39a7e80be 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 a285846fb1a6..e1ca60cd279a 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 919f0d806157..e68da51225af 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 ebc7bcd542a6..9135b8eb4f5c 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 bc9da6efcbf4..887894619706 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 b983112d91e4..5e1da0f77eb8 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 87dfca735cc0..a7923f49d3f8 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 b756df331d0c..af637bf1bba5 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 72d3fa04591c..2c357d120e11 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 c7b9613e1ee4..7eaec8bab78f 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 629d084b2617..29073927dcba 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 8acb0bef4c43..c101d4bae077 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 18869e670f62..3390c3a8c8aa 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 d2b9fba0895e..b9282cce0973 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 c062b9bc2f94..2e44ad45edf6 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 123a686e421c..87d5f01d093f 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 3b0fad592fed..7a10c0e949e3 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 9c18ba6cd654..9bb3a5017dd5 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 000000000000..839d9f5a1430 --- /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 f32ba620732f..000000000000 --- 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 000000000000..a11b45000eb0 --- /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 902c4339cf20..2752aeeadd3b 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 2e579a3dfe88..7f8e385d2825 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 3e3f504444d3..ccc0159055da 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 ca36442543e4..975dc0bd0feb 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 29cd64759a59..14c72619768e 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 a51d6c900672..b3a81e569af7 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 bd0b8dd730f9..9e64135f04ee 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 90d432b70f79..d8b5af6694d7 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 ef569a56a3fe..c4e64a563f06 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 fb4bd4d1dcb7..bacdf5c8f695 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 acceb6662d59..4a28581f2284 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 973fcb68b63e..992a87016619 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 f0058712c41a..b19ffbc380fa 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 c9be526e095a..11a6228d81f4 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 d0c6c4fe86ae..635ae5d94c3d 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 66f900293dd1..51a439fe992c 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 58e6ee43ce74..6f7180924933 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 f9ad2d69264f..367fb8143c47 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 000000000000..7aadeb1b65ab --- /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 a711a585a81b..e16e079afd1f 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 e6aea3e7b332..64705ff72592 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 8bbe9b137abb..c86331eba963 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 95072f81e02b..f056bacfbf45 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 5f5279cc483c..c7c4bb096c79 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 d56faf9d5dec..6dc5846855e4 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 505c785857b8..66548d5e8f06 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 deafab6a9525..2481e8aad9f4 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 ba7388ebb1cc..e62c83e6666f 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 107000a33799..7e23dc67d2f0 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 e19446c15760..5a62352e8d50 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 9821121d8c17..57de0d2c9a95 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 5ff0a002e11e..53d9d899f568 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 000000000000..6c79b40b3ff9 --- /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 6f5973879874..9ffd8dafc5c2 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 80b8cafc20d1..e778e3e7067c 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 e5f464920c3e..c19e6dea3c81 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 64901a541a77..cf92289cf8c6 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 000000000000..80e931802660 --- /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 9c68c0806408..4f9f17ba7e01 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 099f34c13d8a..25b18c9f460c 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 59e3700acf5c..a9fa033ffd4c 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 000000000000..f444d1b73121 --- /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/Unit/Block/Product/View/GalleryOptionsTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php index f4258f16bc77..c7036b9f22d6 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 f38ffcd822cd..6bcf17b32434 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 886b03e0f3c1..73478b80091f 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 974c85b2b5c9..cad43f39f026 --- 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 bca7ab30700a..27d36c19e861 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 3147c682664d..ee524ca2fe52 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 da7f7b3b0fa2..a5501b1b48b9 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 4317607fd661..239e19c84dbf 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 0dc0e23ccb3c..000000000000 --- 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 2dafc07ffee1..000000000000 --- 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 000000000000..f77df110ba9a --- /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 2924bf66949c..42e7cab8fe14 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 799424f2557c..0ec5a48e68aa 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 74267f4239f9..2c794f631b68 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 000000000000..5ed8df758e24 --- /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 e58c88123fc6..e0e4ee74d3db 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 000000000000..2be8a9fe6eaa --- /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 d458ad002648..3e87b4892935 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 34dfd614d8de..b6f5fdfef134 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 c4965b37717a..212366298aff 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 90934c1ab93e..88d950104bfb 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 518830ee6745..99b322ef6307 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 000000000000..1192552e9ebe --- /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 000000000000..1a2d52c35756 --- /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 305f4acd40d8..1144ce1bbe6c 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 f3831e50ef3d..eadf47601585 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 000000000000..3ac9cdad269d --- /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 337182abf084..3f8e6f699d84 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 4421b2991266..73f8d988bf27 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 eeacd0f0970f..9edd98e24468 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 a1b2202309d6..5df7412ccdf6 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 e817bcbb42d2..0805d1e48b2d 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 a5b2944a45fa..7460622cd9a8 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 d786f843e052..1ce8c68449bd 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 12cbcd7031e9..110e7fe56594 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 d292bd126593..b4d4ed12d20b 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 a5870d4023a5..6f1e2e66699a 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 e56804a06de2..18a2bab2a31b 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 cc1a7276c70b..c108525d3048 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 bfea74e7ddd8..7b6ee7e9ae00 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 f3b36e515ac6..5e506e4cb6ae 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 3c6cc849081e..fba1f7f8cbcc 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 a1ad456dc520..954f07629529 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 bd05e48e2338..3481796323a7 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 525a1a4f7643..eb1a190e87bc 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 09342ceb2f60..9171214a1137 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 e22843573d9d..5fc50452bb70 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 afef26aad604..d1e66613c35f 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 b8689cc8868d..c65c0872d087 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 6df29fa25692..d3b4e3136652 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 d67a50875b81..e203d3902db7 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 34f5dd831686..63a998022df8 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 d8b90b454b4a..907f23ac2203 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 356ff17183a5..2bd1714537a0 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 6976086e7489..fc46e5eeb212 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 215b28be0579..2f16e9ccb318 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 000000000000..7930c597adea --- /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 8e69fdfe97eb..fd6b7236fff0 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 000000000000..00ee9f27ca37 --- /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 000000000000..d59497b30af6 --- /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 000000000000..54fd531b9f4d --- /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 000000000000..a9f46c781f59 --- /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 000000000000..c9d684beeb93 --- /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 d7118d71db89..68b051f39c3a 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 b0df8fddff08..d92bf88da818 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 2cbfcafdb674..6ac9f6e919fb 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 0d857604cd04..3ad7d50559d7 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 cddba2e91f70..b1b126e3e05d 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 df725c02eb5b..1a52916a85c0 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 810de0f1f4b5..2f97884d83dc 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 000000000000..656b8ec9cb82 --- /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 359d29509566..79dd1450125e 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 25db5207af28..938f6c359b06 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 044f2890447d..8c85fd43514e 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 000000000000..367724891026 --- /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 3139c3577400..ab9fed035cc3 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 3091cffb619c..d5afac28354b 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 ab0531ad0951..63cd205fd87f 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 a5cc522d7ccf..ca597fc57990 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 000000000000..626695da1e55 --- /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 000000000000..e67dc38218f4 --- /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 a528efcb4a81..d3e30dd48f28 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 16f62a3a4fb1..bc01315036c6 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 30be41072242..3e955ae30345 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 4f1ad5c29152..2d2f2eda9be8 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 000000000000..9941c5a9cbb4 --- /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 2c612da4f433..c4d189cd7cb0 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 000000000000..285a218ef844 --- /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 59970335d3d1..b406c053cd20 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 000000000000..57c844ecfed6 --- /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 000000000000..489742db45f7 --- /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 000000000000..1941d19c7d3d --- /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 fbc4172226c5..6ca37bf9b10e 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 8fb575255fed..7656a593d6ff 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 000000000000..0adaba95ebfa --- /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 0e0fa9d95580..a6fbced9b42c 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 87696c129a71..037245e653af 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 98d895a10c26..3d3875bb5c58 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 4d3dceeb3eb6..505dafc27ab1 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 ec7085acba4e..4c0e54b5c5a0 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -8,13 +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; @@ -47,9 +52,9 @@ */ 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'; - private const HASH_ALGORITHM = 'sha256'; /** * Size of bunch - part of products to save in one step. @@ -262,6 +267,11 @@ class Product extends AbstractEntity */ protected $_mediaGalleryAttributeId = null; + /** + * @var string + */ + private $hashAlgorithm = 'crc32c'; + /** * @var array * @codingStandardsIgnoreStart @@ -298,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', @@ -456,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 = []; @@ -760,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 @@ -810,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) @@ -865,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; @@ -908,7 +925,7 @@ public function __construct( $this->linkProcessor = $linkProcessor ?? ObjectManager::getInstance() ->get(LinkProcessor::class); $this->linkProcessor->addNameToIds($this->_linkNameToId); - + $this->hashAlgorithm = (version_compare(PHP_VERSION, '8.1.0') >= 0) ? 'xxh128' : 'crc32c'; parent::__construct( $jsonHelper, $importExportData, @@ -921,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() @@ -1095,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; @@ -1132,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( @@ -1194,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; } @@ -1217,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 = []; @@ -1228,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 ) ); } @@ -1322,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) { @@ -1423,7 +1452,6 @@ public function saveProductEntity(array $entityRowsIn, array $entityRowsUp) static $entityTable = null; $this->countItemsCreated += count($entityRowsIn); $this->countItemsUpdated += count($entityRowsUp); - if (!$entityTable) { $entityTable = $this->_resourceFactory->create()->getEntityTable(); } @@ -1432,7 +1460,6 @@ public function saveProductEntity(array $entityRowsIn, array $entityRowsUp) } if ($entityRowsIn) { $this->_connection->insertMultiple($entityTable, $entityRowsIn); - $select = $this->_connection->select()->from( $entityTable, array_merge($this->getNewSkuFieldsForSelect(), $this->getOldSkuFieldsForSelect()) @@ -1447,10 +1474,8 @@ public function saveProductEntity(array $entityRowsIn, array $entityRowsUp) $this->skuProcessor->setNewSkuData($sku, $key, $value); } } - - $this->updateOldSku($newProducts); + $this->updateSkuStorage($newProducts); } - return $this; } @@ -1470,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); } /** @@ -1547,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']); @@ -1586,14 +1608,12 @@ public function getImagesFromRow(array $rowData) protected function _saveProducts() { $priceIsGlobal = $this->_catalogData->isPriceGlobal(); - $productLimit = null; - $productsQty = null; - $entityLinkField = $this->getProductEntityLinkField(); - + $previousType = null; + $prevAttributeSet = null; + $productMediaPath = $this->getProductMediaPath(); while ($bunch = $this->_dataSourceModel->getNextUniqueBunch($this->getIds())) { $entityRowsIn = []; $entityRowsUp = []; - $attributes = []; $this->websitesCache = []; $this->categoriesCache = []; $tierPrices = []; @@ -1601,429 +1621,509 @@ protected function _saveProducts() $labelsForUpdate = []; $imagesForChangeVisibility = []; $uploadedImages = []; - $previousType = null; - $prevAttributeSet = null; - $existingImages = $this->getExistingImages($bunch); - $this->addImageHashes($existingImages); - + $attributes = []; foreach ($bunch as $rowNum => $rowData) { - // reset category processor's failed categories array - $this->categoryProcessor->clearFailedCategories(); - - if (!$this->validateRow($rowData, $rowNum)) { - continue; - } - if ($this->getErrorAggregator()->hasToBeTerminated()) { - $this->getErrorAggregator()->addRowToSkip($rowNum); - continue; - } - $rowScope = $this->getRowScope($rowData); - - $urlKey = $this->getUrlKey($rowData); - if (!empty($rowData[self::URL_KEY])) { - // If url_key column and its value were in the CSV file - $rowData[self::URL_KEY] = $urlKey; - } elseif ($this->isNeedToChangeUrlKey($rowData)) { - // If url_key column was empty or even not declared in the CSV file but by the rules it is need to - // be setteed. In case when url_key is generating from name column we have to ensure that the bunch - // of products will pass for the event with url_key column. - $bunch[$rowNum][self::URL_KEY] = $rowData[self::URL_KEY] = $urlKey; - } - - $rowSku = $rowData[self::COL_SKU]; - $rowSkuNormalized = mb_strtolower($rowSku); - - if (null === $rowSku) { - $this->getErrorAggregator()->addRowToSkip($rowNum); - continue; - } - - $storeId = !empty($rowData[self::COL_STORE]) - ? $this->getStoreIdByCode($rowData[self::COL_STORE]) - : Store::DEFAULT_STORE_ID; - $rowExistingImages = $existingImages[$storeId][$rowSkuNormalized] ?? []; - $rowStoreMediaGalleryValues = $rowExistingImages; - $rowExistingImages += $existingImages[Store::DEFAULT_STORE_ID][$rowSkuNormalized] ?? []; - - if (self::SCOPE_STORE == $rowScope) { - // set necessary data from SCOPE_DEFAULT row - $rowData[self::COL_TYPE] = $this->skuProcessor->getNewSku($rowSku)['type_id']; - $rowData['attribute_set_id'] = $this->skuProcessor->getNewSku($rowSku)['attr_set_id']; - $rowData[self::COL_ATTR_SET] = $this->skuProcessor->getNewSku($rowSku)['attr_set_code']; - } - - // 1. Entity phase - if ($this->isSkuExist($rowSku)) { - // existing row - if (isset($rowData['attribute_set_code'])) { - $attributeSetId = $this->catalogConfig->getAttributeSetId( - $this->getEntityTypeId(), - $rowData['attribute_set_code'] - ); - - // wrong attribute_set_code was received - if (!$attributeSetId) { - throw new LocalizedException( - __( - 'Wrong attribute set code "%1", please correct it and try again.', - $rowData['attribute_set_code'] - ) - ); - } - } else { - $attributeSetId = $this->skuProcessor->getNewSku($rowSku)['attr_set_id']; + try { + // reset category processor's failed categories array + $this->categoryProcessor->clearFailedCategories(); + if (!$this->validateRow($rowData, $rowNum)) { + continue; } - - $entityRowsUp[] = [ - 'updated_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT), - 'attribute_set_id' => $attributeSetId, - $entityLinkField => $this->getExistingSku($rowSku)[$entityLinkField] - ]; - } else { - if (!$productLimit || $productsQty < $productLimit) { - $entityRowsIn[strtolower($rowSku)] = [ - 'attribute_set_id' => $this->skuProcessor->getNewSku($rowSku)['attr_set_id'], - 'type_id' => $this->skuProcessor->getNewSku($rowSku)['type_id'], - 'sku' => $rowSku, - 'has_options' => isset($rowData['has_options']) ? $rowData['has_options'] : 0, - 'created_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT), - 'updated_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT), - ]; - $productsQty++; - } else { - $rowSku = null; - // sign for child rows to be skipped + if ($this->getErrorAggregator()->hasToBeTerminated()) { $this->getErrorAggregator()->addRowToSkip($rowNum); continue; } - } - - if (!array_key_exists($rowSku, $this->websitesCache)) { - $this->websitesCache[$rowSku] = []; - } - // 2. Product-to-Website phase - if (!empty($rowData[self::COL_PRODUCT_WEBSITES])) { - $websiteCodes = explode($this->getMultipleValueSeparator(), $rowData[self::COL_PRODUCT_WEBSITES]); - foreach ($websiteCodes as $websiteCode) { - $websiteId = $this->storeResolver->getWebsiteCodeToId($websiteCode); - $this->websitesCache[$rowSku][$websiteId] = true; + $rowScope = $this->getRowScope($rowData); + $urlKey = $this->getUrlKey($rowData); + if (!empty($rowData[self::URL_KEY])) { + // If url_key column and its value were in the CSV file + $rowData[self::URL_KEY] = $urlKey; + } elseif ($this->isNeedToChangeUrlKey($rowData)) { + // If url_key column was empty or even not declared in the CSV file but by the rules it needs + // to be settled. In case when url_key is generating from name column we have to ensure that + // the bunch of products will pass for the event with url_key column. + $bunch[$rowNum][self::URL_KEY] = $rowData[self::URL_KEY] = $urlKey; } - } else { - $product = $this->retrieveProductBySku($rowSku); - if ($product) { - $websiteIds = $product->getWebsiteIds(); - foreach ($websiteIds as $websiteId) { - $this->websitesCache[$rowSku][$websiteId] = true; - } + 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); + continue; + } + $storeId = !empty($rowData[self::COL_STORE]) + ? $this->getStoreIdByCode($rowData[self::COL_STORE]) + : Store::DEFAULT_STORE_ID; + if (self::SCOPE_STORE == $rowScope) { + // set necessary data from SCOPE_DEFAULT row + $rowData[self::COL_TYPE] = $this->skuProcessor->getNewSku($rowSku)['type_id']; + $rowData['attribute_set_id'] = $this->skuProcessor->getNewSku($rowSku)['attr_set_id']; + $rowData[self::COL_ATTR_SET] = $this->skuProcessor->getNewSku($rowSku)['attr_set_code']; + } + $this->saveProductEntityPhase($rowData, $entityRowsUp, $entityRowsIn); + $this->saveProductToWebsitePhase($rowData); + $this->saveProductCategoriesPhase($rowNum, $rowData); + $this->saveProductTierPricesPhase($rowData, $priceIsGlobal, $tierPrices); + $this->saveProductMediaGalleryPhase( + $rowNum, + $rowData, + $storeId, + $existingImages, + $productMediaPath, + $uploadedImages, + $imagesForChangeVisibility, + $labelsForUpdate, + $mediaGallery + ); + $this->saveProductAttributesPhase( + $rowData, + $rowScope, + $previousType, + $prevAttributeSet, + $attributes + ); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch + } catch (Skip $skip) { + // Product is skipped. Go on to the next one. } - - // 3. Categories phase - if (!array_key_exists($rowSku, $this->categoriesCache)) { - $this->categoriesCache[$rowSku] = []; - } - $rowData['rowNum'] = $rowNum; - $categoryIds = $this->processRowCategories($rowData); - foreach ($categoryIds as $id) { - $this->categoriesCache[$rowSku][$id] = true; - } - unset($rowData['rowNum']); - - // 4.1. Tier prices phase - if (!empty($rowData['_tier_price_website'])) { - $tierPrices[$rowSku][] = [ - 'all_groups' => $rowData['_tier_price_customer_group'] == self::VALUE_ALL, - 'customer_group_id' => $rowData['_tier_price_customer_group'] == - self::VALUE_ALL ? 0 : $rowData['_tier_price_customer_group'], - 'qty' => $rowData['_tier_price_qty'], - 'value' => $rowData['_tier_price_price'], - 'website_id' => self::VALUE_ALL == $rowData['_tier_price_website'] || - $priceIsGlobal ? 0 : $this->storeResolver->getWebsiteCodeToId($rowData['_tier_price_website']), - ]; + } + foreach ($bunch as $rowNum => $rowData) { + if ($this->getErrorAggregator()->isRowInvalid($rowNum)) { + unset($bunch[$rowNum]); } + } + $this->saveProductEntity($entityRowsIn, $entityRowsUp); + $this->_saveProductWebsites($this->websitesCache); + $this->_saveProductCategories($this->categoriesCache); + $this->_saveProductTierPrices($tierPrices); + $this->_saveMediaGallery($mediaGallery); + $this->updateMediaGalleryVisibility($imagesForChangeVisibility); + $this->updateMediaGalleryLabels($labelsForUpdate); + $this->_saveProductAttributes($attributes); + $this->_eventManager->dispatch( + 'catalog_product_import_bunch_save_after', + [ + 'adapter' => $this, + 'bunch' => $bunch, + 'media_gallery' => $mediaGallery, + 'media_gallery_labels' => $labelsForUpdate, + ] + ); + } + return $this; + } + //phpcs:enable Generic.Metrics.NestingLevel - if (!$this->validateRow($rowData, $rowNum)) { - continue; - } + // phpcs:enable - // 5. Media gallery phase - list($rowImages, $rowLabels) = $this->getImagesFromRow($rowData); - $imageHiddenStates = $this->getImagesHiddenStates($rowData); - foreach (array_keys($imageHiddenStates) as $image) { - //Mark image as uploaded if it exists - if (array_key_exists($image, $rowExistingImages)) { - $uploadedImages[$image] = $image; - } - //Add image to hide to images list if it does not exist - if (empty($rowImages[self::COL_MEDIA_IMAGE]) - || !in_array($image, $rowImages[self::COL_MEDIA_IMAGE]) - ) { - $rowImages[self::COL_MEDIA_IMAGE][] = $image; - } + /** + * In _saveProducts loop, save product entity + * + * @param array $rowData + * @param array $entityRowsUp + * @param array $entityRowsIn + * @return void + * @throws LocalizedException + */ + private function saveProductEntityPhase(array $rowData, array &$entityRowsUp, array &$entityRowsIn) : void + { + $rowSku = $rowData[self::COL_SKU]; + if ($this->isSkuExist($rowSku)) { + // existing row + if (isset($rowData['attribute_set_code'])) { + $attributeSetId = $this->catalogConfig->getAttributeSetId( + $this->getEntityTypeId(), + $rowData['attribute_set_code'] + ); + // wrong attribute_set_code was received + if (!$attributeSetId) { + throw new LocalizedException( + __( + 'Wrong attribute set code "%1", please correct it and try again.', + $rowData['attribute_set_code'] + ) + ); } + } else { + $attributeSetId = $this->skuProcessor->getNewSku($rowSku)['attr_set_id']; + } + $entityLinkField = $this->getProductEntityLinkField(); + $entityRowsUp[] = [ + 'updated_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT), + 'attribute_set_id' => $attributeSetId, + $entityLinkField => $this->getExistingSku($rowSku)[$entityLinkField] + ]; + } else { + $entityRowsIn[strtolower($rowSku)] = [ + 'attribute_set_id' => $this->skuProcessor->getNewSku($rowSku)['attr_set_id'], + 'type_id' => $this->skuProcessor->getNewSku($rowSku)['type_id'], + 'sku' => $rowSku, + 'has_options' => isset($rowData['has_options']) ? $rowData['has_options'] : 0, + 'created_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT), + 'updated_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT), + ]; + } + } - $rowData[self::COL_MEDIA_IMAGE] = []; - list($rowImages, $rowData) = $this->clearNoSelectionImages($rowImages, $rowData); - - /* - * Note: to avoid problems with undefined sorting, the value of media gallery items positions - * must be unique in scope of one product. - */ - $position = 0; - foreach ($rowImages as $column => $columnImages) { - foreach ($columnImages as $columnImageKey => $columnImage) { - $hash = filter_var($columnImage, FILTER_VALIDATE_URL) - ? $this->getRemoteFileHash($columnImage) - : $this->getFileHash($this->joinFilePaths($this->getUploader()->getTmpDir(), $columnImage)); - $uploadedFile = $this->findImageByHash($rowExistingImages, $hash); - if (!$uploadedFile && !isset($uploadedImages[$columnImage])) { - $uploadedFile = $this->uploadMediaFiles($columnImage); - $uploadedFile = $uploadedFile ?: $this->getSystemFile($columnImage); - if ($uploadedFile) { - $uploadedImages[$columnImage] = $uploadedFile; - } else { - unset($rowData[$column]); - $this->addRowError( - ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE, - $rowNum, - null, - null, - ProcessingError::ERROR_LEVEL_NOT_CRITICAL - ); - } - } elseif (isset($uploadedImages[$columnImage])) { - $uploadedFile = $uploadedImages[$columnImage]; - } + /** + * In _saveProducts loop, save product to website + * + * @param array $rowData + * @return void + */ + private function saveProductToWebsitePhase(array $rowData) : void + { + $rowSku = $rowData[self::COL_SKU]; + if (!array_key_exists($rowSku, $this->websitesCache)) { + $this->websitesCache[$rowSku] = []; + } + if (!empty($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] + : []); - if ($uploadedFile && $column !== self::COL_MEDIA_IMAGE) { - $rowData[$column] = $uploadedFile; - } + foreach ($websiteCodes as $websiteCode) { + $websiteId = $this->storeResolver->getWebsiteCodeToId($websiteCode); + $this->websitesCache[$rowSku][$websiteId] = true; + } + } else { + $product = $this->retrieveProductBySku($rowSku); + if ($product) { + $websiteIds = $product->getWebsiteIds(); + foreach ($websiteIds as $websiteId) { + $this->websitesCache[$rowSku][$websiteId] = true; + } + } + } + } - if (!$uploadedFile || isset($mediaGallery[$storeId][$rowSku][$uploadedFile])) { - continue; - } + /** + * In _saveProducts loop, save product's categories + * + * @param int $rowNum + * @param array $rowData + * @return void + */ + private function saveProductCategoriesPhase(int $rowNum, array $rowData) : void + { + $rowSku = $rowData[self::COL_SKU]; + if (!array_key_exists($rowSku, $this->categoriesCache)) { + $this->categoriesCache[$rowSku] = []; + } + $rowData['rowNum'] = $rowNum; + $categoryIds = $this->processRowCategories($rowData); + foreach ($categoryIds as $id) { + $this->categoriesCache[$rowSku][$id] = true; + } + } - $uploadedFileNormalized = ltrim($uploadedFile, '/\\'); - if (isset($rowExistingImages[$uploadedFileNormalized])) { - $currentFileData = $rowExistingImages[$uploadedFileNormalized]; - $currentFileData['store_id'] = $storeId; - $storeMediaGalleryValueExists = isset($rowStoreMediaGalleryValues[$uploadedFileNormalized]); - if (array_key_exists($uploadedFile, $imageHiddenStates) - && $currentFileData['disabled'] != $imageHiddenStates[$uploadedFile] - ) { - $imagesForChangeVisibility[] = [ - 'disabled' => $imageHiddenStates[$uploadedFile], - 'imageData' => $currentFileData, - 'exists' => $storeMediaGalleryValueExists - ]; - $storeMediaGalleryValueExists = true; - } - - if (isset($rowLabels[$column][$columnImageKey]) - && $rowLabels[$column][$columnImageKey] !== $currentFileData['label'] - ) { - $labelsForUpdate[] = [ - 'label' => $rowLabels[$column][$columnImageKey], - 'imageData' => $currentFileData, - 'exists' => $storeMediaGalleryValueExists - ]; - } - } else { - if ($column === self::COL_MEDIA_IMAGE) { - $rowData[$column][] = $uploadedFile; - } - $mediaGalleryStoreData = [ - 'attribute_id' => $this->getMediaGalleryAttributeId(), - 'label' => isset($rowLabels[$column][$columnImageKey]) - ? $rowLabels[$column][$columnImageKey] - : '', - 'position' => ++$position, - 'disabled' => isset($imageHiddenStates[$columnImage]) - ? $imageHiddenStates[$columnImage] : '0', - 'value' => $uploadedFile, - ]; - $mediaGallery[$storeId][$rowSku][$uploadedFile] = $mediaGalleryStoreData; - // Add record for default scope if it does not exist - if (!($mediaGallery[Store::DEFAULT_STORE_ID][$rowSku][$uploadedFile] ?? [])) { - //Set label and disabled values to their default values - $mediaGalleryStoreData['label'] = null; - $mediaGalleryStoreData['disabled'] = 0; - $mediaGallery[Store::DEFAULT_STORE_ID][$rowSku][$uploadedFile] = $mediaGalleryStoreData; - } - } - } - } + /** + * In _saveProducts loop, save product's tier prices + * + * @param array $rowData + * @param bool $priceIsGlobal + * @param array $tierPrices + * @return void + */ + private function saveProductTierPricesPhase(array $rowData, bool $priceIsGlobal, array &$tierPrices) : void + { + $rowSku = $rowData[self::COL_SKU]; + if (!empty($rowData['_tier_price_website'])) { + $tierPrices[$rowSku][] = [ + 'all_groups' => $rowData['_tier_price_customer_group'] == self::VALUE_ALL, + 'customer_group_id' => $rowData['_tier_price_customer_group'] == + self::VALUE_ALL ? 0 : $rowData['_tier_price_customer_group'], + 'qty' => $rowData['_tier_price_qty'], + 'value' => $rowData['_tier_price_price'], + 'website_id' => self::VALUE_ALL == $rowData['_tier_price_website'] || + $priceIsGlobal ? 0 : $this->storeResolver->getWebsiteCodeToId($rowData['_tier_price_website']), + ]; + } + } - // 6. Attributes phase - $rowStore = (self::SCOPE_STORE == $rowScope) - ? $this->storeResolver->getStoreCodeToId($rowData[self::COL_STORE]) - : 0; - $productType = isset($rowData[self::COL_TYPE]) ? $rowData[self::COL_TYPE] : null; - if ($productType !== null) { - $previousType = $productType; - } - if (isset($rowData[self::COL_ATTR_SET])) { - $prevAttributeSet = $rowData[self::COL_ATTR_SET]; - } - if (self::SCOPE_NULL == $rowScope) { - // for multiselect attributes only - if ($prevAttributeSet !== null) { - $rowData[self::COL_ATTR_SET] = $prevAttributeSet; - } - if ($productType === null && $previousType !== null) { - $productType = $previousType; - } - if ($productType === null) { - continue; + /** + * In _saveProducts loop, save product's media gallery + * + * @param int $rowNum + * @param array $rowData + * @param int $storeId + * @param array $existingImages + * @param string $productMediaPath + * @param array $uploadedImages + * @param array $imagesForChangeVisibility + * @param array $labelsForUpdate + * @param array $mediaGallery + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return void + */ + private function saveProductMediaGalleryPhase( + int $rowNum, + array &$rowData, + int $storeId, + array $existingImages, + string $productMediaPath, + array &$uploadedImages, + array &$imagesForChangeVisibility, + array &$labelsForUpdate, + array &$mediaGallery + ) : void { + $rowSku = $rowData[self::COL_SKU]; + $rowSkuNormalized = mb_strtolower($rowSku); + $rowExistingImages = $existingImages[$storeId][$rowSkuNormalized] ?? []; + $rowStoreMediaGalleryValues = $rowExistingImages; + $rowExistingImages += $existingImages[Store::DEFAULT_STORE_ID][$rowSkuNormalized] ?? []; + list($rowImages, $rowLabels) = $this->getImagesFromRow($rowData); + $imageHiddenStates = $this->getImagesHiddenStates($rowData); + foreach (array_keys($imageHiddenStates) as $image) { + //Mark image as uploaded if it exists + if (array_key_exists($image, $rowExistingImages)) { + $uploadedImages[$image] = $image; + } + //Add image to hide to images list if it does not exist + if (empty($rowImages[self::COL_MEDIA_IMAGE]) + || !in_array($image, $rowImages[self::COL_MEDIA_IMAGE]) + ) { + $rowImages[self::COL_MEDIA_IMAGE][] = $image; + } + } + $rowData[self::COL_MEDIA_IMAGE] = []; + list($rowImages, $rowData) = $this->clearNoSelectionImages($rowImages, $rowData); + /* + * Note: to avoid problems with undefined sorting, the value of media gallery items positions + * must be unique in scope of one product. + */ + $position = 0; + $imagesByHash = []; + foreach ($rowImages as $column => $columnImages) { + foreach ($columnImages as $columnImageKey => $columnImage) { + $uploadedFile = $this->findImageByColumnImage( + $productMediaPath, + $rowExistingImages, + $columnImage, + $imagesByHash + ); + if (!$uploadedFile && !isset($uploadedImages[$columnImage])) { + $uploadedFile = $this->uploadMediaFiles($columnImage); + $uploadedFile = $uploadedFile ?: $this->getSystemFile($columnImage); + if ($uploadedFile) { + $uploadedImages[$columnImage] = $uploadedFile; + } else { + unset($rowData[$column]); + $this->addRowError( + ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE, + $rowNum, + null, + sprintf( + $this->_messageTemplates[ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE], + $columnImage, + $rowNum + ), + ProcessingError::ERROR_LEVEL_NOT_CRITICAL + ); } + } elseif (isset($uploadedImages[$columnImage])) { + $uploadedFile = $uploadedImages[$columnImage]; } - - $productTypeModel = $this->_productTypeModels[$productType]; - if (isset($rowData['tax_class_name']) && strlen($rowData['tax_class_name'])) { - $rowData['tax_class_id'] = - $this->taxClassProcessor->upsertTaxClass($rowData['tax_class_name'], $productTypeModel); + if ($uploadedFile && $column !== self::COL_MEDIA_IMAGE) { + $rowData[$column] = $uploadedFile; } - - if ($this->getBehavior() == Import::BEHAVIOR_APPEND || - empty($rowData[self::COL_SKU]) - ) { - $rowData = $productTypeModel->clearEmptyData($rowData); + if (!$uploadedFile || isset($mediaGallery[$storeId][$rowSku][$uploadedFile])) { + continue; } - - $rowData = $productTypeModel->prepareAttributesWithDefaultValueForSave( - $rowData, - !$this->isSkuExist($rowSku) - ); - $product = $this->_proxyProdFactory->create(['data' => $rowData]); - - foreach ($rowData as $attrCode => $attrValue) { - $attribute = $this->retrieveAttributeByCode($attrCode); - - if ('multiselect' != $attribute->getFrontendInput() && self::SCOPE_NULL == $rowScope) { - // skip attribute processing for SCOPE_NULL rows - continue; + $uploadedFileNormalized = ltrim($uploadedFile, '/\\'); + if (isset($rowExistingImages[$uploadedFileNormalized])) { + $currentFileData = $rowExistingImages[$uploadedFileNormalized]; + $currentFileData['store_id'] = $storeId; + $storeMediaGalleryValueExists = isset($rowStoreMediaGalleryValues[$uploadedFileNormalized]); + if (array_key_exists($uploadedFile, $imageHiddenStates) + && $currentFileData['disabled'] != $imageHiddenStates[$uploadedFile] + ) { + $imagesForChangeVisibility[] = [ + 'disabled' => $imageHiddenStates[$uploadedFile], + 'imageData' => $currentFileData, + 'exists' => $storeMediaGalleryValueExists + ]; + $storeMediaGalleryValueExists = true; } - $attrId = $attribute->getId(); - $backModel = $attribute->getBackendModel(); - $attrTable = $attribute->getBackend()->getTable(); - $storeIds = [0]; - - if ('datetime' == $attribute->getBackendType() - && ( - in_array($attribute->getAttributeCode(), $this->dateAttrCodes) - || $attribute->getIsUserDefined() - ) + if (isset($rowLabels[$column][$columnImageKey]) + && $rowLabels[$column][$columnImageKey] !== $currentFileData['label'] ) { - $attrValue = $this->dateTime->formatDate($attrValue, false); - } elseif ('datetime' == $attribute->getBackendType() && strtotime($attrValue)) { - $attrValue = gmdate( - 'Y-m-d H:i:s', - $this->_localeDate->date($attrValue)->getTimestamp() - ); - } elseif ($backModel) { - $attribute->getBackend()->beforeSave($product); - $attrValue = $product->getData($attribute->getAttributeCode()); + $labelsForUpdate[] = [ + 'label' => $rowLabels[$column][$columnImageKey], + 'imageData' => $currentFileData, + 'exists' => $storeMediaGalleryValueExists + ]; } - 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); - } - } elseif (self::SCOPE_STORE == $attribute->getIsGlobal()) { - $storeIds = [$rowStore]; - } - if (!$this->isSkuExist($rowSku)) { - $storeIds[] = 0; - } + } else { + if ($column === self::COL_MEDIA_IMAGE) { + $rowData[$column][] = $uploadedFile; } - foreach ($storeIds as $storeId) { - if (!isset($attributes[$attrTable][$rowSku][$attrId][$storeId])) { - $attributes[$attrTable][$rowSku][$attrId][$storeId] = $attrValue; - } + $mediaGalleryStoreData = [ + 'attribute_id' => $this->getMediaGalleryAttributeId(), + 'label' => isset($rowLabels[$column][$columnImageKey]) + ? $rowLabels[$column][$columnImageKey] + : '', + 'position' => ++$position, + 'disabled' => isset($imageHiddenStates[$columnImage]) + ? $imageHiddenStates[$columnImage] : '0', + 'value' => $uploadedFile, + ]; + $mediaGallery[$storeId][$rowSku][$uploadedFile] = $mediaGalleryStoreData; + // Add record for default scope if it does not exist + if (!($mediaGallery[Store::DEFAULT_STORE_ID][$rowSku][$uploadedFile] ?? [])) { + //Set label and disabled values to their default values + $mediaGalleryStoreData['label'] = null; + $mediaGalleryStoreData['disabled'] = 0; + $mediaGallery[Store::DEFAULT_STORE_ID][$rowSku][$uploadedFile] = $mediaGalleryStoreData; } - // restore 'backend_model' to avoid 'default' setting - $attribute->setBackendModel($backModel); } } + } + } - foreach ($bunch as $rowNum => $rowData) { - if ($this->getErrorAggregator()->isRowInvalid($rowNum)) { - unset($bunch[$rowNum]); + /** + * In _saveProducts loop, save product's attributes + * + * @param array $rowData + * @param int $rowScope + * @param mixed $previousType + * @param mixed $prevAttributeSet + * @param array $attributes + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @return void + */ + private function saveProductAttributesPhase( + array $rowData, + int $rowScope, + &$previousType, + &$prevAttributeSet, + array &$attributes + ) : void { + $rowSku = $rowData[self::COL_SKU]; + $rowStore = (self::SCOPE_STORE == $rowScope) + ? $this->storeResolver->getStoreCodeToId($rowData[self::COL_STORE]) + : 0; + $productType = isset($rowData[self::COL_TYPE]) ? $rowData[self::COL_TYPE] : null; + if ($productType !== null) { + $previousType = $productType; + } + if (isset($rowData[self::COL_ATTR_SET])) { + $prevAttributeSet = $rowData[self::COL_ATTR_SET]; + } + if (self::SCOPE_NULL == $rowScope) { + // for multiselect attributes only + if ($prevAttributeSet !== null) { + $rowData[self::COL_ATTR_SET] = $prevAttributeSet; + } + if ($productType === null && $previousType !== null) { + $productType = $previousType; + } + if ($productType === null) { + throw new Skip(__('Unknown Product Type')); + } + } + $productTypeModel = $this->_productTypeModels[$productType]; + if (isset($rowData['tax_class_name']) && strlen($rowData['tax_class_name'])) { + $rowData['tax_class_id'] = + $this->taxClassProcessor->upsertTaxClass($rowData['tax_class_name'], $productTypeModel); + } + if ($this->getBehavior() == Import::BEHAVIOR_APPEND || + empty($rowData[self::COL_SKU]) + ) { + $rowData = $productTypeModel->clearEmptyData($rowData); + } + $rowData = $productTypeModel->prepareAttributesWithDefaultValueForSave( + $rowData, + !$this->isSkuExist($rowSku) + ); + $product = $this->_proxyProdFactory->create(['data' => $rowData]); + foreach ($rowData as $attrCode => $attrValue) { + $attribute = $this->retrieveAttributeByCode($attrCode); + if ('multiselect' != $attribute->getFrontendInput() && self::SCOPE_NULL == $rowScope) { + // skip attribute processing for SCOPE_NULL rows + continue; + } + $attrId = $attribute->getId(); + $backModel = $attribute->getBackendModel(); + $attrTable = $attribute->getBackend()->getTable(); + $storeIds = [0]; + if ('datetime' == $attribute->getBackendType() + && ( + in_array($attribute->getAttributeCode(), $this->dateAttrCodes) + || $attribute->getIsUserDefined() + ) + ) { + $attrValue = $this->dateTime->formatDate($attrValue, false); + } elseif ('datetime' == $attribute->getBackendType() && strtotime($attrValue)) { + $attrValue = gmdate( + 'Y-m-d H:i:s', + $this->_localeDate->date($attrValue)->getTimestamp() + ); + } elseif ($backModel) { + $attribute->getBackend()->beforeSave($product); + $attrValue = $product->getData($attribute->getAttributeCode()); + } + if (self::SCOPE_STORE == $rowScope) { + if (self::SCOPE_WEBSITE == $attribute->getIsGlobal()) { + $storeIds = $this->storeResolver->getStoreIdToWebsiteStoreIds($rowStore); + } elseif (self::SCOPE_STORE == $attribute->getIsGlobal()) { + $storeIds = [$rowStore]; + } + if (!$this->isSkuExist($rowSku)) { + $storeIds[] = 0; } } - - $this->saveProductEntity($entityRowsIn, $entityRowsUp) - ->_saveProductWebsites($this->websitesCache) - ->_saveProductCategories($this->categoriesCache) - ->_saveProductTierPrices($tierPrices) - ->_saveMediaGallery($mediaGallery) - ->_saveProductAttributes($attributes) - ->updateMediaGalleryVisibility($imagesForChangeVisibility) - ->updateMediaGalleryLabels($labelsForUpdate); - - $this->_eventManager->dispatch( - 'catalog_product_import_bunch_save_after', - ['adapter' => $this, 'bunch' => $bunch] - ); + foreach ($storeIds as $storeId) { + if (!isset($attributes[$attrTable][$rowSku][$attrId][$storeId])) { + $attributes[$attrTable][$rowSku][$attrId][$storeId] = $attrValue; + } + } + // restore 'backend_model' to avoid 'default' setting + $attribute->setBackendModel($backModel); } - - return $this; } - //phpcs:enable Generic.Metrics.NestingLevel - - // phpcs:enable /** - * Returns image hash by path + * Returns image content by path * * @param string $path * @return string * @throws \Magento\Framework\Exception\FileSystemException */ - private function getFileHash(string $path): string + private function getFileContent(string $path): string { - $content = ''; if ($this->_mediaDirectory->isFile($path) && $this->_mediaDirectory->isReadable($path) ) { - $content = $this->_mediaDirectory->readFile($path); + return $this->_mediaDirectory->readFile($path); } - return $content ? hash(self::HASH_ALGORITHM, $content) : ''; + return ''; } /** - * Returns hash for remote file + * Returns content for remote file * * @param string $filename * @return string */ - private function getRemoteFileHash(string $filename): string - { - $hash = hash_file(self::HASH_ALGORITHM, $filename); - return $hash !== false ? $hash : ''; - } - - /** - * Generate hashes for existing images for comparison with newly uploaded images. - * - * @param array $images - * @return void - */ - private function addImageHashes(array &$images): void + private function getRemoteFileContent(string $filename): string { - $productMediaPath = $this->getProductMediaPath(); - foreach ($images as $storeId => $skus) { - foreach ($skus as $sku => $files) { - foreach ($files as $path => $file) { - $hash = $this->getFileHash($this->joinFilePaths($productMediaPath, $file['value'])); - if ($hash) { - $images[$storeId][$sku][$path]['hash'] = $hash; - } - } - } + try { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $content = file_get_contents($filename); + } catch (\Exception $e) { + $content = false; } + + return $content !== false ? $content : ''; } /** @@ -2080,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) { @@ -2402,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); + } + } } } @@ -2488,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; } /** @@ -2614,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( @@ -2726,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; } @@ -3039,7 +3161,7 @@ protected function getResource() } /** - * Whether a url key is needed to be change. + * Whether a url key needs to change. * * @param array $rowData * @return bool @@ -3139,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; } @@ -3153,7 +3274,7 @@ private function isSkuExist($sku) */ private function getExistingSku($sku) { - return $this->_oldSku[strtolower($sku)]; + return $this->skuStorage->get((string)$sku); } /** @@ -3273,24 +3394,63 @@ private function getRowExistingStockItem(array $rowData): StockItemInterface } /** - * Returns image that matches the provided hash + * Returns image that matches the provided image content * + * @param string $productMediaPath * @param array $images - * @param string $hash + * @param string $columnImage + * @param array $imagesByHash * @return string */ - private function findImageByHash(array $images, string $hash): string - { - $value = ''; - if ($hash) { - foreach ($images as $image) { - if (isset($image['hash']) && $image['hash'] === $hash) { - $value = $image['value']; - break; + private function findImageByColumnImage( + string $productMediaPath, + array &$images, + string $columnImage, + array &$imagesByHash + ): string { + $content = filter_var($columnImage, FILTER_VALIDATE_URL) + ? $this->getRemoteFileContent($columnImage) + : $this->getFileContent($this->joinFilePaths($this->getUploader()->getTmpDir(), $columnImage)); + if (!$content) { + return ''; + } + return $this->findImageByColumnImageUsingHash($productMediaPath, $images, $content, $imagesByHash); + } + + /** + * Returns image that matches the provided image content using hash + * + * @param string $productMediaPath + * @param array $images + * @param string $content + * @param array $imagesByHash + * @return string + */ + private function findImageByColumnImageUsingHash( + string $productMediaPath, + array &$images, + string $content, + array &$imagesByHash + ): string { + $hash = hash($this->hashAlgorithm, $content); + if (!empty($imagesByHash[$hash])) { + return $imagesByHash[$hash]; + } + foreach ($images as &$image) { + if (!isset($image['hash'])) { + $imageContent = $this->getFileContent($this->joinFilePaths($productMediaPath, $image['value'])); + if (!$imageContent) { + $image['hash'] = ''; + continue; } + $image['hash'] = hash($this->hashAlgorithm, $imageContent); + $imagesByHash[$image['hash']] = $image['value']; + } + if (!empty($image['hash']) && $image['hash'] === $hash) { + return $image['value']; } } - return $value; + return ''; } /** diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php index 3a6d8d2533e8..856a985014ff 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 47b5528e956d..9e6292529fff 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 84d36be94900..f7cd0c207fc0 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php @@ -7,14 +7,25 @@ namespace Magento\CatalogImportExport\Model\Import\Product; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Model\ResourceModel\Product\Option\CollectionFactory; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection as ProductOptionValueCollection; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\CollectionFactory as ProductOptionValueCollectionFactory; use Magento\CatalogImportExport\Model\Import\Product; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\ImportExport\Model\Import; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; use Magento\ImportExport\Model\ResourceModel\CollectionByPagesIterator; +use Magento\ImportExport\Model\ResourceModel\CollectionByPagesIteratorFactory; +use Magento\ImportExport\Model\ResourceModel\Helper; +use Magento\ImportExport\Model\ResourceModel\Import\Data; use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; /** * Entity class which provide possibility to import product custom options @@ -102,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 * @@ -318,11 +334,6 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity */ private $productEntityLinkField; - /** - * @var string - */ - private $productEntityIdentifierField; - /** * @var ProductOptionValueCollectionFactory */ @@ -334,26 +345,46 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity private $optionTypeTitles; /** + * @var TransactionManagerInterface|null + */ + private $transactionManager; + + /** + * Contains mapping between new assigned option ID and ID in DB + * * @var array */ - private $lastOptionTitle; + private $optionNewIdExistingIdMap = []; + + /** + * Contains mapping between new assigned option_type ID and ID in DB + * + * @var array + */ + private $optionTypeNewIdExistingIdMap = []; + + /** + * @var SkuStorage + */ + private SkuStorage $skuStorage; /** - * @param \Magento\ImportExport\Model\ResourceModel\Import\Data $importData + * @param Data $importData * @param ResourceConnection $resource - * @param \Magento\ImportExport\Model\ResourceModel\Helper $resourceHelper - * @param \Magento\Store\Model\StoreManagerInterface $_storeManager - * @param \Magento\Catalog\Model\ProductFactory $productFactory - * @param \Magento\Catalog\Model\ResourceModel\Product\Option\CollectionFactory $optionColFactory - * @param \Magento\ImportExport\Model\ResourceModel\CollectionByPagesIteratorFactory $colIteratorFactory + * @param Helper $resourceHelper + * @param StoreManagerInterface $_storeManager + * @param ProductFactory $productFactory + * @param CollectionFactory $optionColFactory + * @param CollectionByPagesIteratorFactory $colIteratorFactory * @param \Magento\Catalog\Helper\Data $catalogData - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $dateTime + * @param ScopeConfigInterface $scopeConfig + * @param TimezoneInterface $dateTime * @param ProcessingErrorAggregatorInterface $errorAggregator * @param array $data - * @param ProductOptionValueCollectionFactory $productOptionValueCollectionFactory - * @throws \Magento\Framework\Exception\LocalizedException - * + * @param ProductOptionValueCollectionFactory|null $productOptionValueCollectionFactory + * @param TransactionManagerInterface|null $transactionManager + * @param SkuStorage|null $skuStorage + * @throws LocalizedException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -369,7 +400,9 @@ public function __construct( \Magento\Framework\Stdlib\DateTime\TimezoneInterface $dateTime, ProcessingErrorAggregatorInterface $errorAggregator, array $data = [], - ProductOptionValueCollectionFactory $productOptionValueCollectionFactory = null + ProductOptionValueCollectionFactory $productOptionValueCollectionFactory = null, + ?TransactionManagerInterface $transactionManager = null, + ?SkuStorage $skuStorage = null ) { $this->_resource = $resource; $this->_catalogData = $catalogData; @@ -381,7 +414,9 @@ public function __construct( $this->_scopeConfig = $scopeConfig; $this->dateTime = $dateTime; $this->productOptionValueCollectionFactory = $productOptionValueCollectionFactory - ?: \Magento\Framework\App\ObjectManager::getInstance()->get(ProductOptionValueCollectionFactory::class); + ?: ObjectManager::getInstance()->get(ProductOptionValueCollectionFactory::class); + $this->transactionManager = $transactionManager + ?: ObjectManager::getInstance()->get(TransactionManagerInterface::class); if (isset($data['connection'])) { $this->_connection = $data['connection']; @@ -409,6 +444,8 @@ public function __construct( } $this->errorAggregator = $errorAggregator; + $this->skuStorage = $skuStorage ?? ObjectManager::getInstance() + ->get(SkuStorage::class); $this->_initSourceEntities($data)->_initTables($data)->_initStores($data); @@ -798,10 +835,12 @@ protected function _findExistingOptionId(array $newOptionData, array $newOptionT ksort($newOptionTitles); $existingOptions = $this->getOldCustomOptions()[$productId]; foreach ($existingOptions as $optionId => $optionData) { - if ($optionData['type'] == $newOptionData['type'] - && $optionData['titles'][Store::DEFAULT_STORE_ID] == $newOptionTitles[Store::DEFAULT_STORE_ID] - ) { - return $optionId; + if ($optionData['type'] == $newOptionData['type']) { + foreach ($newOptionTitles as $storeId => $title) { + if (isset($optionData['titles'][$storeId]) && $optionData['titles'][$storeId] === $title) { + return $optionId; + } + } } } } @@ -897,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] = []; } @@ -916,26 +955,39 @@ 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; + } + /** * Validate secondary custom option row * @@ -957,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; @@ -1126,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++; @@ -1249,13 +1311,16 @@ private function addFileOptions($result, $optionRow) protected function _importData() { $this->_initProductsSku(); - $nextOptionId = $this->_resourceHelper->getNextAutoincrement($this->_tables['catalog_product_option']); - $nextValueId = $this->_resourceHelper->getNextAutoincrement( + $nextOptionId = (int) $this->_resourceHelper->getNextAutoincrement($this->_tables['catalog_product_option']); + $nextValueId = (int) $this->_resourceHelper->getNextAutoincrement( $this->_tables['catalog_product_option_type_value'] ); $prevOptionId = 0; $optionId = null; $valueId = null; + $this->optionNewIdExistingIdMap = []; + $this->optionTypeNewIdExistingIdMap = []; + $prevRowSku = null; while ($bunch = $this->_dataSourceModel->getNextUniqueBunch($this->getIds())) { $products = []; $options = []; @@ -1267,29 +1332,39 @@ protected function _importData() $parentCount = []; $childCount = []; $optionsToRemove = []; + $optionCount = $valueCount = 0; foreach ($bunch as $rowNumber => $rowData) { - if (isset($optionId, $valueId) && - (empty($rowData[PRODUCT::COL_STORE_VIEW_CODE]) || empty($rowData['custom_options'])) - ) { - $nextOptionId = $optionId; - $nextValueId = $valueId; + $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; @@ -1306,8 +1381,9 @@ protected function _importData() $products, $prices ); - if ($optionData != null) { - $options[] = $optionData; + if ($optionData) { + $options[$optionData['option_id']] = $optionData; + $optionCount++; } $this->_collectOptionTypeData( $combinedData, @@ -1319,17 +1395,9 @@ protected function _importData() $parentCount, $childCount ); + $valueCount++; $this->_collectOptionTitle($combinedData, $prevOptionId, $titles); - $this->checkOptionTitles( - $options, - $titles, - $combinedData, - $prevOptionId, - $optionId, - $products, - $prices - ); } } $this->removeExistingOptions($products, $optionsToRemove); @@ -1338,76 +1406,20 @@ protected function _importData() 'prices' => $typePrices, 'titles' => $typeTitles, ]; - $this->setLastOptionTitle($titles); //Save prepared custom options data. $this->savePreparedCustomOptions( $products, - $options, + array_values($options), $titles, $prices, $types ); + $this->optionNewIdExistingIdMap = $this->markNewIdsAsExisting($this->optionNewIdExistingIdMap); + $this->optionTypeNewIdExistingIdMap = $this->markNewIdsAsExisting($this->optionTypeNewIdExistingIdMap); } return true; } - /** - * Check options titles. - * - * If products were split up between bunches, - * this function will add needed option for option titles - * - * @param array $options - * @param array $titles - * @param array $combinedData - * @param int $prevOptionId - * @param int $optionId - * @param array $products - * @param array $prices - * @return void - */ - private function checkOptionTitles( - array &$options, - array &$titles, - array $combinedData, - int &$prevOptionId, - int &$optionId, - array $products, - array $prices - ) : void { - $titlesCount = count($titles); - if ($titlesCount > 0 && count($options) !== $titlesCount) { - $combinedData[Product::COL_STORE_VIEW_CODE] = ''; - $optionId--; - $option = $this->_collectOptionMainData( - $combinedData, - $prevOptionId, - $optionId, - $products, - $prices - ); - if ($option) { - $options[] = $option; - } - } - } - - /** - * Setting last Custom Option Title - * to use it later in _collectOptionTitle - * to set correct title for default store view - * - * @param array $titles - */ - private function setLastOptionTitle(array &$titles) : void - { - if (count($titles) > 0) { - end($titles); - $key = key($titles); - $this->lastOptionTitle[$key] = $titles[$key]; - } - } - /** * Remove existing options. * @@ -1436,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; @@ -1469,9 +1476,7 @@ protected function _collectOptionMainData( $optionData = null; if ($this->_rowIsMain) { - $optionData = empty($rowData[Product::COL_STORE_VIEW_CODE]) - ? $this->_getOptionData($rowData, $this->_rowProductId, $nextOptionId, $this->_rowType) - : ''; + $optionData = $this->_getOptionData($rowData, $this->_rowProductId, $nextOptionId, $this->_rowType); if (!$this->_isRowHasSpecificType($this->_rowType) && ($priceData = $this->_getPriceData($rowData, $nextOptionId, $this->_rowType)) @@ -1519,38 +1524,21 @@ protected function _collectOptionTypeData( array &$childCount ) { if ($this->_isRowHasSpecificType($this->_rowType) && $prevOptionId) { - $specificTypeData = $this->_getSpecificTypeData($rowData, $nextValueId); - //For default store + $specificTypeData = $this->_getSpecificTypeData([self::COLUMN_STORE => null] + $rowData, $nextValueId); if ($specificTypeData) { - $typeValues[$prevOptionId][] = $specificTypeData['value']; - - // ensure default title is set - if (!isset($typeTitles[$nextValueId][Store::DEFAULT_STORE_ID])) { - $typeTitles[$nextValueId][Store::DEFAULT_STORE_ID] = $specificTypeData['title']; - } + $typeValues[$prevOptionId][$nextValueId] = $specificTypeData['value']; + $typeTitles[$nextValueId][$this->_rowStoreId] = $specificTypeData['title']; - if ($specificTypeData['price']) { + if (!empty($specificTypeData['price'])) { if ($this->_isPriceGlobal) { $typePrices[$nextValueId][Store::DEFAULT_STORE_ID] = $specificTypeData['price']; } else { - // ensure default price is set - if (!isset($typePrices[$nextValueId][Store::DEFAULT_STORE_ID])) { - $typePrices[$nextValueId][Store::DEFAULT_STORE_ID] = $specificTypeData['price']; - } $typePrices[$nextValueId][$this->_rowStoreId] = $specificTypeData['price']; } } $nextValueId++; } - $specificTypeData = $this->_getSpecificTypeData($rowData, 0, false); - //For others stores - if ($specificTypeData) { - if (isset($specificTypeData['price'])) { - $typePrices[$nextValueId][$this->_rowStoreId] = $specificTypeData['price']; - } - $typeTitles[$nextValueId++][$this->_rowStoreId] = $specificTypeData['title']; - } } } @@ -1564,16 +1552,7 @@ protected function _collectOptionTypeData( */ protected function _collectOptionTitle(array $rowData, $prevOptionId, array &$titles) { - $defaultStoreId = Store::DEFAULT_STORE_ID; if (!empty($rowData[self::COLUMN_TITLE])) { - if (!isset($titles[$prevOptionId][$defaultStoreId])) { - if (isset($this->lastOptionTitle[$prevOptionId])) { - $titles[$prevOptionId] = $this->lastOptionTitle[$prevOptionId]; - unset($this->lastOptionTitle); - } else { - $titles[$prevOptionId][$defaultStoreId] = $rowData[self::COLUMN_TITLE]; - } - } $titles[$prevOptionId][$this->_rowStoreId] = $rowData[self::COLUMN_TITLE]; } } @@ -1592,7 +1571,10 @@ protected function _compareOptionsWithExisting(array &$options, array &$titles, { foreach ($options as &$optionData) { $newOptionId = $optionData['option_id']; - if ($optionId = $this->_findExistingOptionId($optionData, $titles[$newOptionId])) { + $optionId = $this->optionNewIdExistingIdMap[$newOptionId] + ?? $this->_findExistingOptionId($optionData, $titles[$newOptionId]); + $this->optionNewIdExistingIdMap[$newOptionId] = $optionId ?: null; + if ($optionId && (int) $optionId !== (int) $newOptionId) { $optionData['option_id'] = $optionId; $titles[$optionId] = $titles[$newOptionId]; unset($titles[$newOptionId]); @@ -1600,6 +1582,8 @@ protected function _compareOptionsWithExisting(array &$options, array &$titles, foreach ($prices[$newOptionId] as $storeId => $priceStoreData) { $prices[$newOptionId][$storeId]['option_id'] = $optionId; } + $prices[$optionId] = $prices[$newOptionId]; + unset($prices[$newOptionId]); } if (isset($typeValues[$newOptionId])) { $typeValues[$optionId] = $typeValues[$newOptionId]; @@ -1627,8 +1611,10 @@ private function restoreOriginalOptionTypeIds(array &$typeValues, array &$typePr foreach ($optionTypes as &$optionType) { $optionTypeId = $optionType['option_type_id']; foreach ($typeTitles[$optionTypeId] as $storeId => $optionTypeTitle) { - $existingTypeId = $this->getExistingOptionTypeId($optionId, $storeId, $optionTypeTitle); - if ($existingTypeId) { + $existingTypeId = $this->optionTypeNewIdExistingIdMap[$optionTypeId] + ?? $this->getExistingOptionTypeId($optionId, $storeId, $optionTypeTitle); + $this->optionTypeNewIdExistingIdMap[$optionTypeId] = $existingTypeId ?: null; + if ($existingTypeId && (int) $existingTypeId !== (int) $optionTypeId) { $optionType['option_type_id'] = $existingTypeId; $typeTitles[$existingTypeId] = $typeTitles[$optionTypeId]; unset($typeTitles[$optionTypeId]); @@ -1812,7 +1798,7 @@ protected function _getPriceData(array $rowData, $optionId, $type) ) { $priceData = [ 'option_id' => $optionId, - 'store_id' => $this->_rowStoreId, + 'store_id' => $this->_isPriceGlobal ? Store::DEFAULT_STORE_ID : $this->_rowStoreId, 'price_type' => 'fixed', ]; @@ -1920,7 +1906,12 @@ protected function _saveOptions(array $options) protected function _saveTitles(array $titles) { $titleRows = []; + $existingOptionIds = array_flip(array_filter($this->optionNewIdExistingIdMap)); foreach ($titles as $optionId => $storeInfo) { + // Check that if it is a new option, then make sure a record for default store will be created + if (!isset($existingOptionIds[$optionId]) && count($storeInfo) > 0) { + $storeInfo = [Store::DEFAULT_STORE_ID => reset($storeInfo)] + $storeInfo; + } //for use default $uniqStoreInfo = array_unique($storeInfo); foreach ($uniqStoreInfo as $storeId => $title) { @@ -1948,7 +1939,12 @@ protected function _savePrices(array $prices) { if ($prices) { $optionPriceRows = []; - foreach ($prices as $storesData) { + $existingOptionIds = array_flip(array_filter($this->optionNewIdExistingIdMap)); + foreach ($prices as $optionId => $storesData) { + // Check that if it is a new option, then make sure a record for default store will be created + if (!isset($existingOptionIds[$optionId]) && count($storesData) > 0) { + $storesData = [Store::DEFAULT_STORE_ID => reset($storesData)] + $storesData; + } foreach ($storesData as $row) { $optionPriceRows[] = $row; } @@ -1973,8 +1969,6 @@ protected function _savePrices(array $prices) */ protected function _saveSpecificTypeValues(array $typeValues) { - $this->_deleteSpecificTypeValues(array_keys($typeValues)); - $typeValueRows = []; foreach ($typeValues as $optionId => $optionInfo) { foreach ($optionInfo as $row) { @@ -1983,7 +1977,7 @@ protected function _saveSpecificTypeValues(array $typeValues) } } if ($typeValueRows) { - $this->_connection->insertMultiple($this->_tables['catalog_product_option_type_value'], $typeValueRows); + $this->_connection->insertOnDuplicate($this->_tables['catalog_product_option_type_value'], $typeValueRows); } return $this; @@ -1998,7 +1992,12 @@ protected function _saveSpecificTypeValues(array $typeValues) protected function _saveSpecificTypePrices(array $typePrices) { $optionTypePriceRows = []; + $existingOptionTypeIds = array_flip(array_filter($this->optionTypeNewIdExistingIdMap)); foreach ($typePrices as $optionTypeId => $storesData) { + // Check that if it is a new option value, then make sure a record for default store will be created + if (!isset($existingOptionTypeIds[$optionTypeId]) && count($storesData) > 0) { + $storesData = [Store::DEFAULT_STORE_ID => reset($storesData)] + $storesData; + } foreach ($storesData as $storeId => $row) { $row['option_type_id'] = $optionTypeId; $row['store_id'] = $storeId; @@ -2025,7 +2024,12 @@ protected function _saveSpecificTypePrices(array $typePrices) protected function _saveSpecificTypeTitles(array $typeTitles) { $optionTypeTitleRows = []; + $existingOptionTypeIds = array_flip(array_filter($this->optionTypeNewIdExistingIdMap)); foreach ($typeTitles as $optionTypeId => $storesData) { + // Check that if it is a new option value, then make sure a record for default store will be created + if (!isset($existingOptionTypeIds[$optionTypeId]) && count($storesData) > 0) { + $storesData = [Store::DEFAULT_STORE_ID => reset($storesData)] + $storesData; + } //for use default $uniqStoresData = array_unique($storesData); foreach ($uniqStoresData as $storeId => $title) { @@ -2116,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. * @@ -2124,6 +2161,7 @@ protected function _parseCustomOptions($rowData) public function clearProductsSkuToId() { $this->_productsSkuToId = null; + $this->resetProductsSkus = true; return $this; } @@ -2142,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. * @@ -2180,14 +2203,35 @@ private function savePreparedCustomOptions( $this->_compareOptionsWithExisting($options, $titles, $prices, $types['values']); $this->restoreOriginalOptionTypeIds($types['values'], $types['prices'], $types['titles']); } - - $this->_saveOptions($options) - ->_saveTitles($titles) - ->_savePrices($prices) - ->_saveSpecificTypeValues($types['values']) - ->_saveSpecificTypePrices($types['prices']) - ->_saveSpecificTypeTitles($types['titles']) - ->_updateProducts($products); + $this->transactionManager->start($this->_connection); + try { + $this->_saveOptions($options) + ->_saveTitles($titles) + ->_savePrices($prices) + ->_saveSpecificTypeValues($types['values']) + ->_saveSpecificTypePrices($types['prices']) + ->_saveSpecificTypeTitles($types['titles']) + ->_updateProducts($products); + $this->transactionManager->commit(); + } catch (\Throwable $exception) { + $this->transactionManager->rollBack(); + throw $exception; + } } } + + /** + * Mark new IDs as existing IDs + * + * @param array $idsMap + * @return array + */ + private function markNewIdsAsExisting(array $idsMap): array + { + $newIds = array_keys(array_filter($idsMap, 'is_null')); + return array_replace( + $idsMap, + array_combine($newIds, $newIds) + ); + } } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Skip.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Skip.php new file mode 100644 index 000000000000..6416456dabc5 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Skip.php @@ -0,0 +1,15 @@ +<?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\Framework\Exception\LocalizedException; +use Magento\Framework\Phrase; + +class Skip extends LocalizedException +{ +} 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 000000000000..539085737f43 --- /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 b01c8417111e..862cd89e3bda 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 e425f3c88f4c..f74886069d50 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 8df5afce568f..b143f768c464 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 4ad763b9134a..190ec8ee43ca 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 4e48eafcbc67..7a5c77453aaa 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 d8a926f7cfe3..000000000000 --- 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 0d0d4ea80530..000000000000 --- 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 87020be7cd30..000000000000 --- 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 c83045b2062c..000000000000 --- 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 000000000000..d2c8ab302923 --- /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 fbf66a5d2af0..b48650e59d84 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 2eede59757f9..8044dc65fb0c 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 fe7ed943c95c..4e3c3f0048ab 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 57b78481686c..9eea5ad24f81 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 bbfc65d18c7e..8477af9601a2 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 c45ff33d11be..f57413d390d6 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/Unit/Model/Import/Product/CategoryProcessorTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php index c2ce4c6499ec..9e3a2f220f73 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 2a35cc8874d7..5df2a80e0df2 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 d60a18057a42..b20793c0e79b 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,11 +12,13 @@ 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; use Magento\Framework\Data\Collection\Db\FetchStrategyInterface; use Magento\Framework\Data\Collection\EntityFactory; +use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\EntityMetadata; use Magento\Framework\EntityManager\MetadataPool; @@ -141,7 +143,7 @@ class OptionTest extends AbstractImportTestCase */ protected $_expectedOptions = [ [ - 'option_id' => 1, + 'option_id' => 2, 'sku' => '1-text', 'max_characters' => '100', 'file_extension' => null, @@ -150,10 +152,10 @@ class OptionTest extends AbstractImportTestCase 'product_id' => 1, 'type' => 'field', 'is_require' => 1, - 'sort_order' => 0 + 'sort_order' => 1 ], [ - 'option_id' => 2, + 'option_id' => 3, 'sku' => '2-date', 'max_characters' => 0, 'file_extension' => null, @@ -162,10 +164,10 @@ class OptionTest extends AbstractImportTestCase 'product_id' => 1, 'type' => 'date_time', 'is_require' => 1, - 'sort_order' => 0 + 'sort_order' => 2 ], [ - 'option_id' => 3, + 'option_id' => 4, 'sku' => '', 'max_characters' => 0, 'file_extension' => null, @@ -174,10 +176,10 @@ class OptionTest extends AbstractImportTestCase 'product_id' => 1, 'type' => 'drop_down', 'is_require' => 1, - 'sort_order' => 0 + 'sort_order' => 3 ], [ - 'option_id' => 4, + 'option_id' => 5, 'sku' => '', 'max_characters' => 0, 'file_extension' => null, @@ -186,7 +188,7 @@ class OptionTest extends AbstractImportTestCase 'product_id' => 1, 'type' => 'radio', 'is_require' => 1, - 'sort_order' => 0 + 'sort_order' => 4 ] ]; @@ -233,6 +235,11 @@ class OptionTest extends AbstractImportTestCase */ protected $metadataPoolMock; + /** + * @var SkuStorage + */ + private $skuStorageMock; + /** * Init entity adapter model * @@ -282,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), @@ -297,7 +307,9 @@ protected function setUp(): void ProcessingErrorAggregatorInterface::class ), $this->_getModelDependencies($addExpectations, $deleteBehavior, $doubleOptions), - $optionValueCollectionFactoryMock + $optionValueCollectionFactoryMock, + $this->createMock(\Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface::class), + $this->skuStorageMock ]; $modelClassName = Option::class; @@ -337,22 +349,20 @@ protected function _getModelDependencies( bool $deleteBehavior = false, bool $doubleOptions = false ): array { - $connection = $this->getMockBuilder(\stdClass::class)->addMethods( - ['delete', 'quoteInto', 'insertMultiple', 'insertOnDuplicate'] - ) + $connection = $this->getMockBuilder(AdapterInterface::class) ->disableOriginalConstructor() - ->getMock(); + ->getMockForAbstractClass(); if ($addExpectations) { if ($deleteBehavior) { $connection->expects( - $this->exactly(2) + $this->exactly(1) )->method( 'quoteInto' )->willReturnCallback( [$this, 'stubQuoteInto'] ); $connection->expects( - $this->exactly(2) + $this->exactly(1) )->method( 'delete' )->willReturnCallback( @@ -360,14 +370,7 @@ protected function _getModelDependencies( ); } else { $connection->expects( - $this->once() - )->method( - 'insertMultiple' - )->willReturnCallback( - [$this, 'verifyInsertMultiple'] - ); - $connection->expects( - $this->exactly(6) + $this->exactly(7) )->method( 'insertOnDuplicate' )->willReturnCallback( @@ -455,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 ); @@ -618,6 +633,12 @@ public function verifyInsertMultiple(string $table, array $data): void public function verifyInsertOnDuplicate(string $table, array $data, array $fields = []): void { switch ($table) { + case $this->_tables['catalog_product_option']: + $this->assertEquals($this->_expectedOptions, $data); + break; + case $this->_tables['catalog_product_option_type_value']: + $this->assertEquals($this->_expectedTypeValues, $data); + break; case $this->_tables['catalog_product_option_title']: $this->assertEquals($this->_expectedTitles, $data); $this->assertEquals(['title'], $fields); diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/_files/product_with_custom_options.csv b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/_files/product_with_custom_options.csv index 92cad357803c..02cc55ef3de6 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/_files/product_with_custom_options.csv +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/_files/product_with_custom_options.csv @@ -1,2 +1,2 @@ -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,,,,,,,,10,,,,,new-product,,,,,,,,,,,,,,,,,,,,,,,,"name=Test Field Title,type=field,required=1;sku=1-text,price=0,price_type=fixed|name=Test Date and Time Title,type=date_time,required=1,price=2,option_title=custom option 1,sku=2-date|name=Test Select,type=drop_down,required=1,price=3,option_title=Option 1,sku=3-1-select|name=Test Select,type=drop_down,required=1,price=3,option_title=Option 2,sku=3-2-select|name=Test Radio,type=radio,required=1,price=3,option_title=Option 1,sku=4-1-radio|name=Test Radio,type=radio,required=1,price=3,option_title=Option 2,sku=4-2-radio",,,,,,,,,,,,,,,,,,,,,,,,Block after Info Column,,, +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,,,,,,,,10,,,,,new-product,,,,,,,,,,,,,,,,,,,,,,,,"name=Test Field Title,type=field,required=1;sku=1-text,price=0,price_type=fixed,max_characters=100|name=Test Date and Time Title,type=date_time,required=1,price=2,option_title=custom option 1,sku=2-date|name=Test Select,type=drop_down,required=1,price=3,option_title=Option 1,sku=3-1-select|name=Test Select,type=drop_down,required=1,price=3,option_title=Option 2,sku=3-2-select|name=Test Radio,type=radio,required=1,price=3,option_title=Option 1,sku=4-1-radio|name=Test Radio,type=radio,required=1,price=3,option_title=Option 2,sku=4-2-radio",,,,,,,,,,,,,,,,,,,,,,,,Block after Info Column,,, 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 5d4555747f0b..31b4db67b48f 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 c5678553d9c0..730f73668502 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 d5ae17d5c392..000000000000 --- 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 3659cde191b5..000000000000 --- 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 43fdda6227ac..4150fca46fa6 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 522b478752f0..05f8ceb5425d 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 4b9383b9eb10..aa5af91b75fb 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 e0375471acf1..508b9377cc09 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 d280df7e9fe1..f6a73f30741e 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 4b42c6498c94..da38af7ad4ae 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 c3649496f2be..d2bb8f645637 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 10123c9c5a10..56c931edccde 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 e530b0d83c9c..a4773db7d20b 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 5d5f22580b1e..ad543c5e3e5d 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 4436f3b220c2..54ffab34e8c2 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 5c3c82701339..245cebc6e4a7 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 e3288d355f74..c9fef8f0f611 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 19c5f597d4b3..9ff9497c705d 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 41b96b0d5ccd..d81a933fb042 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 a3fca303236b..ec0906ca5471 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 07bf2746338d..3021dca1e391 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 f38d4a2ca91b..0492ba1cb548 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 ad7291281ed3..1b09a1a39b86 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 cd26a575b676..a558d834be03 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 b120b93c9193..6f609cf18fb1 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 e7918e32f78a..5008836c2997 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 79aa47b33ea1..407c338c0ae4 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 909ec9346ebf..6bbfdfff3017 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 cb7d68c92ef6..e743ac6cda21 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 96bf5bd96535..1ee8e1a97e89 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 e79d2098be68..cc47f912ddd5 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 c2715241fbe1..53630dcbbc96 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 8b0849c8874b..9df634c225d7 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 c871a8dee65f..c5b71d934580 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 59c4722c3aad..ac4690d46be8 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 f104552b4e0f..ea4c35de053b 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 600bf9897a03..363f91916fb7 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 fceb079b1abe..7236278df024 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 4a78babd0320..db31c47b8470 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 e111a5267da7..ed762bdf3fa1 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 f109643bc09c..b1c35950304a 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 adf62b75b2ad..007ecff49a2f 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 3922670f175e..1c0d18f786f2 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 59d359433c26..dbbde539d573 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 bc9b8471ccd8..84997907c4f2 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 bbba3498ab03..e1abb020b9ac 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 2cc69513f31b..156520e4aa8e 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 936cafb60f33..09537cdd5c44 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 5d218a4f0651..4aaa5435b9b6 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 8238c1e8f6b2..50e16ad5ed04 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 77d85034f14d..597b8ad9160d 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 db5e6cff7425..791ad9a07954 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/Observer/ReindexQuoteInventoryObserver.php b/app/code/Magento/CatalogInventory/Observer/ReindexQuoteInventoryObserver.php index e22ec886474c..a2a981009437 100644 --- a/app/code/Magento/CatalogInventory/Observer/ReindexQuoteInventoryObserver.php +++ b/app/code/Magento/CatalogInventory/Observer/ReindexQuoteInventoryObserver.php @@ -4,41 +4,58 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogInventory\Observer; +use Magento\CatalogInventory\Model\Indexer\Stock\Processor as StockProcessor; +use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceProcessor; use Magento\Framework\Event\Observer as EventObserver; use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Exception\LocalizedException; +use Psr\Log\LoggerInterface; +/** + * Responsible for re-indexing stock items after a successful order. + */ class ReindexQuoteInventoryObserver implements ObserverInterface { /** - * @var \Magento\CatalogInventory\Model\Indexer\Stock\Processor + * @var StockProcessor */ - protected $stockIndexerProcessor; + private StockProcessor $stockIndexerProcessor; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\Processor + * @var PriceProcessor */ - protected $priceIndexer; + private PriceProcessor $priceIndexer; /** - * @var \Magento\CatalogInventory\Observer\ItemsForReindex + * @var ItemsForReindex */ - protected $itemsForReindex; + private ItemsForReindex $itemsForReindex; /** - * @param \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor - * @param \Magento\Catalog\Model\Indexer\Product\Price\Processor $priceIndexer + * @var LoggerInterface + */ + private LoggerInterface $logger; + + /** + * @param StockProcessor $stockIndexerProcessor + * @param PriceProcessor $priceIndexer * @param ItemsForReindex $itemsForReindex + * @param LoggerInterface $logger */ public function __construct( - \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor, - \Magento\Catalog\Model\Indexer\Product\Price\Processor $priceIndexer, - ItemsForReindex $itemsForReindex + StockProcessor $stockIndexerProcessor, + PriceProcessor $priceIndexer, + ItemsForReindex $itemsForReindex, + LoggerInterface $logger ) { $this->stockIndexerProcessor = $stockIndexerProcessor; $this->priceIndexer = $priceIndexer; $this->itemsForReindex = $itemsForReindex; + $this->logger = $logger; } /** @@ -47,37 +64,43 @@ public function __construct( * @param EventObserver $observer * @return void */ - public function execute(EventObserver $observer) + public function execute(EventObserver $observer): void { - // Reindex quote ids - $quote = $observer->getEvent()->getQuote(); - $productIds = []; - foreach ($quote->getAllItems() as $item) { - $productIds[$item->getProductId()] = $item->getProductId(); - $children = $item->getChildrenItems(); - if ($children) { - foreach ($children as $childItem) { - $productIds[$childItem->getProductId()] = $childItem->getProductId(); + try { + // Reindex quote ids + $quote = $observer->getEvent()->getData('quote'); + $productIds = []; + foreach ($quote->getAllItems() as $item) { + $productIds[$item->getData('product_id')] = $item->getData('product_id'); + $children = $item->getData('children_items'); + if ($children) { + foreach ($children as $childItem) { + $productIds[$childItem->getData('product_id')] = $childItem->getData('product_id'); + } } } - } - if ($productIds) { - $this->stockIndexerProcessor->reindexList($productIds); - } + if ($productIds) { + $this->stockIndexerProcessor->reindexList($productIds); + } - // Reindex previously remembered items - $productIds = []; - foreach ($this->itemsForReindex->getItems() as $item) { - $item->save(); - $productIds[] = $item->getProductId(); - } + // Reindex previously remembered items + $productIds = []; + foreach ($this->itemsForReindex->getItems() as $item) { + $item->save(); + $productIds[] = $item->getData('product_id'); + } - if (!empty($productIds)) { - $this->priceIndexer->reindexList($productIds); - } + if (!empty($productIds)) { + $this->priceIndexer->reindexList($productIds); + } - $this->itemsForReindex->clear(); - // Clear list of remembered items - we don't need it anymore + $this->itemsForReindex->clear(); + // Clear list of remembered items - we don't need it anymore + } catch (LocalizedException $exception) { + $this->logger->error('Error while re-indexing order items: ' . $exception->getLogMessage()); + $this->stockIndexerProcessor->markIndexerAsInvalid(); + $this->priceIndexer->markIndexerAsInvalid(); + } } } 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 000000000000..286a1f875d97 --- /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 6f388c3e6c6d..9a198dd571de 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 26dd08be0a8c..f4e3d6ede81d 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 cd1931cf7fb7..7d14f1b62694 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 000000000000..beea664659dc --- /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 951ca2b0ee80..bed3c41edc51 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 2d239c0f33a6..99cb74052796 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 e17c8fe65d4c..c7beaa3c6835 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/Unit/Model/Indexer/Stock/CacheCleanerTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php index 794f5d92da1e..22dce1a60060 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 24f46c2414f3..9591b84b4c8d 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 44ce1fe6a345..df9d3ee94dbb 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 a60939da60bc..32d5cc93db47 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/CatalogInventory/Test/Unit/Observer/ReindexQuoteInventoryObserverTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Observer/ReindexQuoteInventoryObserverTest.php new file mode 100644 index 000000000000..97156eb4f178 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Unit/Observer/ReindexQuoteInventoryObserverTest.php @@ -0,0 +1,193 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CatalogInventory\Test\Unit\Observer; + +use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceProcessor; +use Magento\CatalogInventory\Model\Indexer\Stock\Processor as StockProcessor; +use Magento\CatalogInventory\Observer\ItemsForReindex; +use Magento\CatalogInventory\Observer\ReindexQuoteInventoryObserver; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Framework\Exception\LocalizedException; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Item; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +/** + * Test class for ReindexQuoteInventoryObserver + */ +class ReindexQuoteInventoryObserverTest extends TestCase +{ + /** + * @var StockProcessor + */ + private StockProcessor $stockIndexerProcessor; + + /** + * @var PriceProcessor + */ + private PriceProcessor $priceIndexer; + + /** + * @var ItemsForReindex + */ + private ItemsForReindex $itemsForReindex; + + /** + * @var LoggerInterface + */ + private LoggerInterface $logger; + + /** + * @var Observer + */ + private Observer $observedObject; + + /** + * @var Event + */ + private Event $event; + + /** + * @var Quote + */ + private Quote $quote; + + /** + * @var Item + */ + private Item $quoteItem; + + /** + * @var ReindexQuoteInventoryObserver + */ + private ReindexQuoteInventoryObserver $sut; + + /** + * @inheritDoc + */ + public function setUp(): void + { + $this->stockIndexerProcessor = $this->createMock(StockProcessor::class); + $this->priceIndexer = $this->createMock(PriceProcessor::class); + $this->itemsForReindex = $this->createMock(ItemsForReindex::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->observedObject = $this->createMock(Observer::class); + $this->event = $this->createMock(Event::class); + $this->quote = $this->createMock(Quote::class); + $this->quoteItem = $this->createMock(Item::class); + + $this->sut = new ReindexQuoteInventoryObserver( + $this->stockIndexerProcessor, + $this->priceIndexer, + $this->itemsForReindex, + $this->logger + ); + } + + /** + * Test execute should re-index quote stock items. + * + * @test + * + * @return void + */ + public function execute(): void + { + $this->observedObject->expects($this->once()) + ->method('getEvent') + ->willReturn($this->event); + + $this->event->expects($this->once()) + ->method('getData') + ->with('quote') + ->willReturn($this->quote); + + $this->quote->expects($this->once()) + ->method('getAllItems') + ->willReturn([$this->quoteItem]); + + $this->quoteItem->expects($this->exactly(6)) + ->method('getData') + ->withConsecutive( + ['product_id'], + ['product_id'], + ['children_items'], + ['product_id'], + ['product_id'], + ['product_id'] + )->willReturnOnConsecutiveCalls(1, 1, [$this->quoteItem], 1, 1, 1); + + $this->stockIndexerProcessor->expects($this->once()) + ->method('reindexList') + ->with([1 => 1]); + + $this->itemsForReindex->expects($this->once()) + ->method('getItems') + ->willReturn([$this->quoteItem]); + + $this->priceIndexer->expects($this->once()) + ->method('reindexList') + ->with([1]); + + $this->itemsForReindex->expects($this->once()) + ->method('clear'); + + $this->sut->execute($this->observedObject); + } + + /** + * Test execute should log error on exception. + * + * @test + * + * @return void + */ + public function executeShouldLogOnException(): void + { + $this->observedObject->expects($this->once()) + ->method('getEvent') + ->willReturn($this->event); + + $this->event->expects($this->once()) + ->method('getData') + ->with('quote') + ->willReturn($this->quote); + + $this->quote->expects($this->once()) + ->method('getAllItems') + ->willReturn([$this->quoteItem]); + + $this->quoteItem->expects($this->exactly(3)) + ->method('getData') + ->withConsecutive( + ['product_id'], + ['product_id'], + ['children_items'] + )->willReturnOnConsecutiveCalls(1, 1, []); + + $this->stockIndexerProcessor->expects($this->once()) + ->method('reindexList') + ->with([1 => 1]) + ->willThrowException(new LocalizedException(__('error'))); + + $this->logger->expects($this->once()) + ->method('error') + ->with('Error while re-indexing order items: error'); + + $this->stockIndexerProcessor->expects($this->once()) + ->method('markIndexerAsInvalid'); + + $this->priceIndexer->expects($this->once()) + ->method('markIndexerAsInvalid'); + + $this->sut->execute($this->observedObject); + } +} 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 000000000000..8459c75f15c8 --- /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 2fd4b36520b0..5d1f91f96283 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 c0fbe2534de8..320eb8a38ba6 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 da32801ace47..1eca8469db1c 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/Setup/Patch/Schema/CleanUpProductPriceReplicaTable.php b/app/code/Magento/CatalogRule/Setup/Patch/Schema/CleanUpProductPriceReplicaTable.php new file mode 100644 index 000000000000..476c7fe277db --- /dev/null +++ b/app/code/Magento/CatalogRule/Setup/Patch/Schema/CleanUpProductPriceReplicaTable.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogRule\Setup\Patch\Schema; + +use Magento\Framework\Setup\SchemaSetupInterface; +use Magento\Framework\Setup\Patch\SchemaPatchInterface; + +class CleanUpProductPriceReplicaTable implements SchemaPatchInterface +{ + /** + * @var SchemaSetupInterface + */ + private SchemaSetupInterface $schemaSetup; + + /** + * CleanUpProductPriceReplicaTable constructor. + * @param SchemaSetupInterface $schemaSetup + */ + public function __construct( + SchemaSetupInterface $schemaSetup + ) { + $this->schemaSetup = $schemaSetup; + } + + /** + * @inheritDoc + */ + public function apply(): void + { + $connection = $this->schemaSetup->startSetup(); + $connection = $this->schemaSetup->getConnection(); + + // There was a bug which caused the catalogrule_product_price_replica + // table to become unnecessarily large. The bug causing the growth has + // been resolved. This schema patch cleans up the damage done by that + // bug on existing websites. Deleting all entries from the replica table + // is safe. + // See https://github.com/magento/magento2/issues/31752 for details. + + $tableName = $connection->getTableName('catalogrule_product_price_replica'); + + if ($connection->isTableExists($tableName)) { + $connection->truncateTable($tableName); + } + + $connection->endSetup(); + } + + /** + * @inheritDoc + */ + public static function getDependencies(): array + { + return []; + } + + /** + * @inheritDoc + */ + public function getAliases(): array + { + return []; + } +} diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogPriceRuleByProductAttributeTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogPriceRuleByProductAttributeTest.xml index 724664917fec..b4665d3a3791 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 26e1966ece36..80fdfd825011 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 d3a349fb3a19..0cc0849d0a0c 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 ee32fa1901f4..9efd5df5b976 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 ce8d2dd1507f..a74b066c71d2 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 2be55819a100..d2e04b8ed77d 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 77228dde8797..c80b2b56fc35 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 1d4b21cb04a6..b97191a37c65 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 c6a3291561fa..8fc1fbff31b9 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 c6452612f82a..89566c39c8b4 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 15322481ae34..a83147bd7087 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 1951aa6c0f6a..5648b17662bf 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 c1d98a7e7128..8a354a29c1ee 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 de69559bb568..f69ec7fe828a 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 b58ddd65c5a9..c2853d019f9d 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 1e0e17e59f73..48e8c46c855a 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 ba446380a4f6..e0607db9c4be 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 1f21c37a3682..bd6c9835ce2a 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 702e046272cb..307ce7a4846a 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 c127f19db374..cd621aa58cc1 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 000000000000..99d2ca3fb6fe --- /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 a616a7ab172f..a3cfdf934a8e 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 c3078a052116..8880a1788336 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 703b3655480c..c13c85d34792 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/Unit/Model/Indexer/ReindexRuleProductTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php index ca3b8be20eda..a2f4ccfad3b5 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 45c9db38c5dd..c8a4c7a59e34 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 a4be621ad513..87008a2f4ea2 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 8ce3cd748238..e3c6a779b1cf 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 b20bd34106e0..f13026fd8623 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 4f13e8d2b6aa..ec40415f7ed6 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 6f9761fedecb..13a8f4a62e96 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 f014c6d13318..1904367ec3d6 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 33ef89e28507..996da0386652 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 9b66606d37a9..d8d4f158ecaa 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 7e9be408a385..10e72e0155ff 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 5a543b363c99..c06144d6aab9 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 000000000000..de4c44a8ea89 --- /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 72358cd002f4..c3374d4b6967 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 6361c076ce17..1163f90989eb 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 c376456a64ac..a0d3d60dc5a7 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 3719899d39ec..dd429fcec645 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 96b9714a343c..972ecd669a63 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 911ed45b82f7..8006ea9d34c7 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 9312eeb1c107..6e21c8f21375 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 02e8e30f3778..99e90ed63a41 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 7b0835302cbd..e6d32b5e9cc7 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 7e13be4bf7b6..1c1bec03778d 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 120f2fff7633..ae85deaafde2 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 fd77b1f6b4ae..ed62a7108f2d 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 5a487e3f0fd4..d8fd296cf725 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 54823c177d00..d9b948ebb8e8 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 c4461eb31039..4d139143a262 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 b52cd9fc4388..19329ccc2433 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 6f21f79145a3..d39d6c5a13de 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 ad817a03c2e2..a891e9bb3e79 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 b2b6bbb47309..b85c92c2a708 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 62f4e3da1059..6b573ac37a2e 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 7f21972ce801..3da958b34130 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 1e777ab0ab66..4bfaadae442e 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 8388e84c32cd..84303b9de145 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 b2b4ef9cc478..db6c10c7e45f 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 45cec0a89936..af72b38ebb0f 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 33dff8aefa33..4a24b0aa83d4 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 c4622d02a515..1130368ba50a 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 ca5e23709968..c70cd5802aa8 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 7508830e0f05..05b19a53c6b7 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 e1f297b6dffe..174a6f298546 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 cceac0475aa7..e7940a50bb80 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 e1a59fef1fdd..4b7b7dc63784 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 b724644f54ef..8c4836bbecf6 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 d54090576128..11deec4a95d6 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 cfff1d1b3bdc..099786f2d3ef 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/Unit/Model/Autocomplete/DataProviderTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Autocomplete/DataProviderTest.php index 18d18352b8d8..5282212cacb9 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 241f00de825d..f35198b5b480 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 2fabec670a57..3faab1dcd395 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 000000000000..013b45313296 --- /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 376e4ced4d5a..9a84bf4c458d 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 569de155c6e3..6b6f68d0bdc5 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 cb9f3fecb4bc..3948e1ca3f18 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 a70f533fbe5b..9aa560f2aa3f 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 39e4c1f0f201..44f183b5de8b 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 fbacddac1ce0..d3c032a5c26a 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 e68b38b046af..f82c8a99ac7f 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 f439c4afe378..7546ad493311 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 244aaf4d5cdc..7b49114f9609 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 f5bf41766623..00e4da2744a0 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 c0e605da6d2c..9d49b22319af 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 000000000000..4563931bc453 --- /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 000000000000..3f3f834518a8 --- /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 e1b59c07d187..203e653cec88 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 d3471e0e4c0b..42aee273c70a 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 12caaca76976..cc3874a1008b 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 26996223417b..cf2f832babeb 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 749f713c1f34..370fc3e778c4 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/Unit/Model/ProductScopeRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php index e6a99bddcbc1..16cf430ac330 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 8573e15e4602..a643c61a6bd5 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 843fb53914fe..7887995db956 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 aad692e68ff1..3f621c589980 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 c4f7066340dc..34b30dd48545 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 f1cec1c15d86..947237cbe084 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 db84784bab5b..a91c7b4c966b 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 204080488488..0b3ba29611d5 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 d35590efc93b..4ca4bc1e2dc7 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 2ac5e5d1aeff..d152b92a2d21 100644 --- a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php +++ b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php @@ -9,9 +9,10 @@ */ namespace Magento\CatalogWidget\Model\Rule\Condition; -use Magento\Catalog\Api\Data\ProductInterface; 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 @@ -201,32 +202,63 @@ protected function addNotGlobalAttribute( \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute, Collection $collection ) { - $storeId = $this->storeManager->getStore()->getId(); - $values = $collection->getAllAttributeValues($attribute); - $validEntities = []; - if ($values) { - foreach ($values as $entityId => $storeValues) { - if (isset($storeValues[$storeId])) { - if ($this->validateAttribute($storeValues[$storeId])) { - $validEntities[] = $entityId; - } - } else { - if (isset($storeValues[Store::DEFAULT_STORE_ID]) && - $this->validateAttribute($storeValues[Store::DEFAULT_STORE_ID]) - ) { - $validEntities[] = $entityId; - } - } - } + $connection = $this->_productResource->getConnection(); + switch ($attribute->getBackendType()) { + case 'decimal': + case 'datetime': + case 'int': + case 'varchar': + case 'text': + $aliasDefault = 'at_' . $attribute->getAttributeCode() . '_default'; + $aliasStore = 'at_' . $attribute->getAttributeCode(); + $collection->addAttributeToSelect($attribute->getAttributeCode(), 'left'); + break; + default: + $aliasDefault = 'at_' . sha1($this->getId()) . $attribute->getAttributeCode() . '_default'; + $aliasStore = 'at_' . sha1($this->getId()) . $attribute->getAttributeCode(); + + $storeDefaultId = $connection->getIfNullSql( + $aliasDefault . '.store_id', + Store::DEFAULT_STORE_ID + ); + $storeId = $connection->getIfNullSql( + $aliasStore . '.store_id', + $this->storeManager->getStore()->getId() + ); + $linkField = $attribute->getEntity()->getLinkField(); + + $collection->getSelect()->joinLeft( + [$aliasDefault => $collection->getTable($attribute->getBackendTable())], + "($aliasDefault.$linkField = e.$linkField) AND ($aliasDefault.store_id = $storeDefaultId)" . + " AND ($aliasDefault.attribute_id = {$attribute->getId()})", + [] + ); + $collection->getSelect()->joinLeft( + [$aliasStore => $collection->getTable($attribute->getBackendTable())], + "($aliasStore.$linkField = e.$linkField) AND ($aliasStore.store_id = $storeId)" . + " AND ($aliasStore.attribute_id = {$attribute->getId()})", + [] + ); } - $this->setOperator('()'); - $this->unsetData('value_parsed'); - if ($validEntities) { - $this->setData('value', implode(',', $validEntities)); + + $fromPart = $collection->getSelect()->getPart(Select::FROM); + if (isset($fromPart[$aliasStore]['joinType']) + && isset($fromPart[$aliasDefault]['joinType']) + ) { + $conditionCheck = $connection->quoteIdentifier($aliasStore . '.value_id') . " > 0"; + $conditionTrue = $connection->quoteIdentifier($aliasStore . '.value'); + $conditionFalse = $connection->quoteIdentifier($aliasDefault . '.value'); + $joinedAttribute = $collection->getSelect()->getConnection()->getCheckSql( + $conditionCheck, + $conditionTrue, + $conditionFalse + ); } else { - $this->unsetData('value'); + $joinedAttribute = $aliasStore . '.' . 'value'; } + $this->joinedAttributes[$attribute->getAttributeCode()] = $joinedAttribute; + return $this; } @@ -290,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 ea1951198c74..b80085640a2d 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/Test/Unit/Model/Rule/Condition/ProductTest.php b/app/code/Magento/CatalogWidget/Test/Unit/Model/Rule/Condition/ProductTest.php index d683912d09fb..49b4c1a7978a 100644 --- a/app/code/Magento/CatalogWidget/Test/Unit/Model/Rule/Condition/ProductTest.php +++ b/app/code/Magento/CatalogWidget/Test/Unit/Model/Rule/Condition/ProductTest.php @@ -189,7 +189,12 @@ public function getMappedSqlFieldPriceDataProvider(): array [ false, false, - 'e.entity_id' + 'at_price.value' + ], + [ + false, + true, + 'price_index.min_price' ], ]; } @@ -248,7 +253,7 @@ public function getBindArgumentValueDataProvider(): array 1 => 2 ] ], - new \Zend_Db_Expr('1, 3') + '2' ], [ [ 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 000f3ffd3693..2a96a0b44c48 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 bfc408d920ad..ed953cf2ca4f 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 000000000000..a01bf234ec93 --- /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 4b411e61ddaf..c529f04243fc 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 027394497e82..9237e1280a8c 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 b1194a25ab54..e7d0cb98e280 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 41867d3c8350..d82a7cb90b9d 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 0addbf069cba..3a2beb3b4371 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 f397a8ddc9cf..f08c48c55efa 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 25e2f0ba4e00..d26b3efae1c3 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 000000000000..e1184376f3a8 --- /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 000000000000..3691b25c3082 --- /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 000000000000..1644bf945cd4 --- /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 000000000000..6888fe0a3ffe --- /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 000000000000..3a2fcd96119c --- /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 942e35ec4d77..d4d45b9ea66f 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 0ba652ba14dd..136fcad9d1fb 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 000000000000..3f0af46dd5cf --- /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/AssertMiniCartEmptyActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertMiniCartEmptyActionGroup.xml index ca891a1dc263..2119e5c43f7b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertMiniCartEmptyActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertMiniCartEmptyActionGroup.xml @@ -16,6 +16,6 @@ <wait stepKey="waitForMinicartAjaxCallToComplete" time="15"/> <dontSeeElement selector="{{StorefrontMinicartSection.productCount}}" stepKey="dontSeeMinicartProductCount"/> <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="expandMinicart"/> - <see selector="{{StorefrontMinicartSection.minicartContent}}" userInput="You have no items in your shopping cart." stepKey="seeEmptyCartMessage"/> + <see selector="{{StorefrontMinicartSection.messageEmptyCart}}" userInput="You have no items in your shopping cart." stepKey="seeEmptyCartMessage"/> </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 5cf9f009ba37..1f8a526a22d2 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 300e9c60ff36..427e4ac4546c 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 1ec42033a782..a0d818d92d22 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 4f9555d84898..f70be8458a7b 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 7633f969fd36..dcca26c63f5d 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 000000000000..91b91e0e439b --- /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 527afdc26a5f..be2c8989c67a 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 340bec9a7dc4..adccda06651b 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 aa48a7c2537b..a1498f5ff128 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 919a2d38dfe9..ee06ec3ef1b4 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 9ce14338f122..5cf5f514bc34 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 3db019c44dd0..931fbd767c6d 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 441e3571d0f5..51214a2a7c6b 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 0c4cea142b4e..fd30fbd834df 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 4b6680442a47..415983485989 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 a532f36e9367..6273699b25a2 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 95d78777ed92..aff2bef43393 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 2147f837d0ab..2466fde666ce 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 e28eef9df6e0..a8da1f70d6a2 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 f13850357b18..077e4eb96029 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 e8949a186466..57822da53150 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 524e3f784ed3..9b2101e2fd8f 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 a55db2b92e9c..79b4d7c08c58 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 59e8b857a54e..e0b5995e1d16 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 96d40ba0fefc..ee91191a6335 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 84f9a7930d40..c3c3a5f855f4 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 f30569535b0c..a989e1767979 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 13db791d3f47..95aad2a9ddf9 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 082eaf38122e..63d54b85c05b 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 d15b89e58a55..e5e912af7334 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 baee6cc7177c..e029bf9ceb9b 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 3482a45b6fa9..2e587e3f7962 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 5a065e5dead9..b30108c4f63b 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 eddb7d430387..278b1b3f3696 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 12a524d2d6ad..1f97e2230ad0 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 f08640d895b6..e90d4c2bd052 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 81de8664f98e..1a32120bad3b 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 979976caf78a..e3cbad4ad279 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 9a3a59095246..e336a65e61c6 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 dc5a63d0d759..0c59e61c609f 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 54ac1143b357..9adbf4dd2e36 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 13e8cba7003b..f84c9a80fba9 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 92a4b9563ab3..39bd49b19630 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 de04ecf4fd51..50e159666253 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 555f7768b1c4..32c33686789a 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 64980c74dc0c..21d3ce6e5472 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 a30f118bd620..efc29bb6a4e3 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 9958b12ceaf2..467ce4c963d1 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 e7b61415723c..21c4807b6adb 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 b82df28ebb95..1a63544c44bc 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 39b4e66ef9f0..f37f6cba7219 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 34f82268b922..909857fd77dd 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 d45fb9274454..5330d4b1a2fb 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 64f392d39edc..f9ddee8284d8 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 138fbe5055d6..5f5b90d11d13 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 f6db22cbccaa..8ccb7af5a334 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 000000000000..8854458b5314 --- /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 b8b8155159d3..b3b0c993bca6 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 90bf2c1465e4..cb7a586faed4 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 2e1c8d5a2788..ab003f05bb39 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 68dcf6600f49..6ef158f8f371 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 4b66c72563d1..8bf060575696 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 c29e19275f75..c02199dd93ca 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 a9d34db16b50..3d680ebf60d7 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 678929ff228c..e1e0a307f9cd 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 699340e1694e..4ead8927a0da 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 a403f928229b..a25040497c2c 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 6718a566d523..1298747458db 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 feab5625c115..4778a4024e4d 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 7e142597e47b..427cf230da74 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 6b9c1f8f9b00..2e06d1533e65 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 000000000000..5f381465bb5b --- /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 024e1221d95e..aa5a3511e00e 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 736e045f588a..6af8b1653503 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 000000000000..7aa0259d4362 --- /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 83ed32803654..a64e667acb70 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 e31db8ee28c7..13c65c7242f9 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 c68961c3e8c2..a6368d71c28b 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 e769d9d37286..53b1a0938e35 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 07e34da20109..9f215c0f96d9 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 93d1c4092c05..386360e2cdbc 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 ee32ce6d928a..dd8675e4e853 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 743f4e016515..f0650fb187d1 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 1ea3f118f9f2..eae50dcb91b5 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 acb274886a6c..20d4f73257aa 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 1a85bb0bee1e..78b389a94e57 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 68842ee09a85..960fb9804306 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 a8c694f4a843..e93afa3c597d 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 ece88e88817b..0eb3fed17131 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 f12dd6fb3482..c2645f3e4d41 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 28e779f802cd..266c8210c273 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 24ca488ea25e..1cb472fcc99f 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 eb76748a81c9..d59af6328f73 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 48059ef66d47..cb99cf7b291d 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 d289bdc0dc8d..97aa966aa4ad 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 4d0196aebf4c..b73bad007d66 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 2d6f36c78edf..ddf65a0922bb 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 000000000000..9c17074e0b9d --- /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 45eb3443e420..2a51c69d4a69 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 cf21af3daed1..9ef5990509b3 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 da4a1b93691b..5879131ff91a 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 6abc2b92178e..789cb9d39528 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 d6b27b73601e..0e08f75afed3 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 34a1a27edd90..0b8118cdb7b9 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 ad4dbd0ab804..39912510b0c4 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 000000000000..a6af2310c4cc --- /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 a5a3675ea0a0..287c737e24e6 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 d83550a82a87..d097a53585ec 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 502a564a22ff..4dc2aaa19c49 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 66a4f417aed9..98dfc2b4f26a 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 3ab3a0b4ad3f..78928a29c5d9 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 000000000000..1d6e4e96dfea --- /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 463a55f59c79..f50706ff5012 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 87eba009f5e5..83f410c7dc36 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 44bfe81b40dc..4b5b9913895b 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 43dd3ead0160..df101420723c 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 05dff1ae5877..7afa1899594b 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 f0c3a23a8d39..b6d193dd4a6a 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 afb4ff03a4fc..291c22408ba0 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 65f5dd365b21..4a90b01a700b 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 e7dd7a0db223..2e9ed2626632 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 8fc37bdaafde..918c737f67de 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 901c5c3598db..03ab7042ca7d 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 000000000000..3317a72e9f61 --- /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 41b5f734d009..af275e148102 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 6e484c30fa81..4ee8a0b1c209 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 97906cade542..bfa8557698a8 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 6016893ed3e2..828814e0bfe7 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 000000000000..b41382ca011f --- /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 66f8e327b9d2..71bb7d1e6595 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/Unit/Model/Backpressure/WebapiRequestTypeExtractorTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Backpressure/WebapiRequestTypeExtractorTest.php new file mode 100644 index 000000000000..a55920fb1cf2 --- /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 7fe8eced6eda..c12cbdec2828 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 d6feb38dc601..52c9eb739d5d 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 b56566a043c3..5bb0f37f3bc2 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 eac0bd849da3..c85d68b35f71 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 280944dc4090..7d57d7be4b73 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 000000000000..2f426d96b4eb --- /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 000000000000..2f426d96b4eb --- /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 aa3cf0748cb0..7e87fe3f7e00 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 b20b4d02706f..411726607c66 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 71e6c39b4e31..fec149418b0a 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 3748212da918..e5509761b1ab 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 36d1d649ecbf..a0a5cf7e92ee 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 3486a9273661..8c11c3ef719a 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 9b2cbcb7a873..0666ac0244e0 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 8b07c02e4d38..8edb5d20c3a2 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 51ebb9dbf11d..c33228670a26 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 d23e220aa994..ab2e495730a1 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 3d31bffd1b54..628bfa165013 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 000000000000..fec0a686d839 --- /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 000000000000..3cddd2ebb538 --- /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 9489fece3700..280de82c83c5 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 f32f1b11926a..a8d806d1b5ab 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 c8f49adc3006..48ad5fae0165 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 c40f24836c81..79f228179e51 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 0172ffc77138..5fd439c0ce24 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 000000000000..7f0150b274f4 --- /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 326f9dcce432..b80a4b83c502 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 cb3e98949c62..e62148ad30a9 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 c597d3d660dc..a8bf09509b6c 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 a90c3536ec74..f0c2bac75982 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 e74235dba19d..21795a7aac5d 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 3eb1e9dd02c9..916046e16680 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 175d5eb62150..0e7074d1a180 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 83ce4df697e4..53c2b8663bd5 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 f9d60796d042..a8b8c742a472 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 198a9fe3fc7b..18f52b197b3f 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 f82840bc07c7..1613bab85edb 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 000000000000..9b7670dc54bb --- /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/Cms/Block/Adminhtml/Block/Widget/Chooser.php b/app/code/Magento/Cms/Block/Adminhtml/Block/Widget/Chooser.php index 86976f6c912e..897ce651146b 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 b30beae73dce..6eeb80be375e 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 249010fbf90c..ba26f0520c56 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 000000000000..c25a0b58c9c9 --- /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 000000000000..2ac8d5d3379e --- /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 f22367393030..7d30908a2d9d 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 c986670009b0..2389289adc7f 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 96886a995b1c..6aafb010fb62 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 b53408bb777e..6d055b3a3a01 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 000000000000..356848855d2a --- /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 7934f52cdf34..23e55f1b01a4 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 000000000000..3054d6eb3141 --- /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 000000000000..7ff5cb3dabec --- /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 000000000000..0afce4f1dc22 --- /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 52a5757ec7b9..5f6625be8409 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 000000000000..a88556a14168 --- /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 bb276b2adb0d..de70c5706360 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 5be91f61e1e1..c9ef757ca747 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 000000000000..0eb511beb2a0 --- /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 40e78c83cf90..03c74d35a9e1 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 b33c3a2b9077..03635122e620 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 24fd7fe82d6a..8aaad14cc37f 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 a63b3abc9f49..fadf602ced9d 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 7001acdea89b..1e19432678e0 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 000000000000..b6b1bb49ba67 --- /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 4f9f9db6b9bc..45918877516e 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 0d483e21499f..9ad4df9e5229 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 cb79113fe591..ca07a5cdd0bb 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 53bb2619075a..b6165f5c5955 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 03a168adf490..f324b5d1a413 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 000000000000..b06bb0341d51 --- /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 7d3946ea86c9..51f62376439f 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 0a7d794b6d17..900b3ec4341b 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 4ac851b8b2a1..eabcdf9e84c9 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 6f9861cd18dc..99b06033763a 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 9e83b02d9184..99cff6935ec1 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 815a217291dd..f2358b6c6224 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 245b1486058b..0fffdf35a0ee 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 dbc821165cb7..edb9d274af6f 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 33e614e566c2..d156b0117c4e 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 8c15d6f4c24c..8224203a67d3 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/Unit/Controller/Noroute/IndexTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Noroute/IndexTest.php index 665b79fdf48b..6f1998ac9802 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 000000000000..eaabe0772781 --- /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 bc291b865c6e..6193b1f96871 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 15b9fe408d22..312496f2683f 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 e2ef86b7f650..aa1b812561a2 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 000000000000..d4cce9f7d58f --- /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 000000000000..1a48504dac22 --- /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 000000000000..79028dde3346 --- /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 07b7261823d9..ea6e6152cacc 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 000000000000..86efef7b2f96 --- /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 78c1071d8e07..ebd089496429 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 4fca42430d16..535374716eb7 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 000000000000..f862e4ca6090 --- /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 1f1b1ca78253..a1c20e3daefb 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 000000000000..b34da19e2ad8 --- /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 000000000000..940775764c62 --- /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 c6b0e4b05f16..b0839b233f8e 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 7025217d1186..ae856ab8f177 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 ed1c38ab33a3..92215c13a679 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 10f9af9268ae..39a383bfbadc 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 641db6d035ca..e02d5e5c4442 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 522ed73fa37d..66d680127f53 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 f278a07cc680..82002bb2bd36 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 2465eecec71d..ed60f63717da 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 445fd8e67937..86c4ee418d60 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 000000000000..d161ae06fa3a --- /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 000000000000..fa4c2db4e02d --- /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 91f93e02dc65..c0820bc36c84 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 f5188d7a419b..2ba52091161f 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 625c3cf2f41f..fa48abcc6d26 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 024d963927e1..6344714ad678 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 594a9df719da..79805f288beb 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 000000000000..d82063f7c86c --- /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 000000000000..e672081551d4 --- /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 000000000000..7b33c5229a86 --- /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 000000000000..0cae2ad05b5b --- /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 000000000000..1b5549b7fb0d --- /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 000000000000..055715d71f93 --- /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 000000000000..99bf20b3e671 --- /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 3f768bdac805..b98041d8d6ed 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 000000000000..3a825ebfb511 --- /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 378aa0bfc510..59c70ff68f41 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 000000000000..aedf461a188c --- /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 72675414576c..f5172e080bbe 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 99a76a446aaa..e01d37f6eea2 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 878a0c24f733..f971b4dd03ce 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 f65f626f1a52..e6ccdc8061e2 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 000000000000..d28db0165a29 --- /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 72a23f7e811c..c1131d2d701e 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 a16208c0e61b..db275df5b302 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 5b9e9405f02a..2237fde67fe1 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 dc3db6ab926f..acdaa8111de4 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 0a322457ed74..1a4fa9915cc4 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 dce63cc449c0..82e25361b0e5 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 deb2c4ed4a48..478e75e3f06e 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 189fbdf69a7e..1a1104aced16 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 c45b31807b70..e8eb823f9fef 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 ceb1efdc8b77..ef3ae9fa3601 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 072b9788d9b7..0abef084d011 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 478c5e59c286..5f0a59ba937a 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 2f1a178d54b7..591b5813985d 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 da9f1316c6bd..d4b33aa36c04 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 ef0ada5e7d5c..309dbd8845a7 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/Plugin/ProductRepositorySave.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductRepositorySave.php index dc4ad39752e4..f14fb67c3f6a 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductRepositorySave.php +++ b/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductRepositorySave.php @@ -49,7 +49,7 @@ public function __construct( * @param ProductRepositoryInterface $subject * @param ProductInterface $product * @param bool $saveOptions - * @return array + * @return void * @throws InputException * @throws NoSuchEntityException * @@ -59,34 +59,23 @@ public function beforeSave( ProductRepositoryInterface $subject, ProductInterface $product, $saveOptions = false - ): array { - $result[] = $product; - if ($product->getTypeId() !== Configurable::TYPE_CODE) { - return $result; - } - + ): void { $extensionAttributes = $product->getExtensionAttributes(); - if ($extensionAttributes === null) { - return $result; - } - - $configurableLinks = (array) $extensionAttributes->getConfigurableProductLinks(); - $configurableOptions = (array) $extensionAttributes->getConfigurableProductOptions(); + if ($extensionAttributes !== null && $product->getTypeId() === Configurable::TYPE_CODE) { + $configurableLinks = (array) $extensionAttributes->getConfigurableProductLinks(); + $configurableOptions = (array) $extensionAttributes->getConfigurableProductOptions(); - if (empty($configurableLinks) && empty($configurableOptions)) { - return $result; - } - - $attributeCodes = []; - /** @var OptionInterface $configurableOption */ - foreach ($configurableOptions as $configurableOption) { - $eavAttribute = $this->productAttributeRepository->get($configurableOption->getAttributeId()); - $attributeCode = $eavAttribute->getAttributeCode(); - $attributeCodes[] = $attributeCode; + if (!empty($configurableLinks) || !empty($configurableOptions)) { + $attributeCodes = []; + /** @var OptionInterface $configurableOption */ + foreach ($configurableOptions as $configurableOption) { + $eavAttribute = $this->productAttributeRepository->get($configurableOption->getAttributeId()); + $attributeCode = $eavAttribute->getAttributeCode(); + $attributeCodes[] = $attributeCode; + } + $this->validateProductLinks($attributeCodes, $configurableLinks); + } } - $this->validateProductLinks($attributeCodes, $configurableLinks); - - return $result; } /** diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/SaveHandler.php b/app/code/Magento/ConfigurableProduct/Model/Product/SaveHandler.php index 1c470808824a..f2294079d829 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 7f228caeb3e4..c2f95bebdb88 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 df2a9707f18d..96d245358ded 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 000000000000..782bce8772c7 --- /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 2f333e7ca6f6..b213d38e2ba2 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 6434cf65bfd6..06dc3a21dfbd 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 a6a6b8753824..25f1a464e3b5 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 b0cc21d1bc77..d495fca96f40 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 000000000000..cc0b31a5b3c9 --- /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 c5050827a94b..4b0e515a2dbe 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 000000000000..1f9eb4d4bd3a --- /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 000000000000..38865284a101 --- /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 b05099da8e85..b7f2cca88920 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 d2873e79a8b8..de0225d509d6 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 3c5581d496b9..a0de07cd5c5a 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 ddb62ea9601a..357ae0185f17 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 07ed24d9bdbc..0c55153004d7 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 356393ad8b63..031818b2ac4e 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 80adfebb57f7..747002b55029 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 e82efcf81135..2950e430ed09 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 9dcfbd8d750c..2f3e7b0837e6 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 f4cad6590e1f..cc6eaad0ce36 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 c6c0dbb3682f..606a01c14a51 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 7dfd0bffaa3c..84f4f5c53bcc 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 9e558659229c..6048972adad3 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 60bc6182b09b..7c1e86105492 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 33a77a96a6bc..6b17e2a8ea5d 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 b2fe25e9691a..66c505c297de 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 6093e39e899c..4edc7e2e0950 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 68d60dfa90e6..98ab451c80c3 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 3e3268cb76f2..f3b91e1bd564 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 c179812ad024..847b52a2e5a5 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 893cfd3fa533..50cdf888c40b 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 6311eaa9f2f9..b72dbfc0f2a9 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 421fcb0c0326..c296e2315978 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 ba120d75f8e6..ae92242eb439 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 ad7cae3e9133..28424ab17a5e 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 a741272bfca0..9458226c2a55 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 f7e171c23f8d..19a3f1faf8a4 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 08ab165f9568..91d520c8e5a0 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 6dab60aaaafa..edd712d086ff 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 a2a1cc3afb6b..00ee45634e0c 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 18940dc56ba6..685f8f9fa634 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 ae6de82987a9..f342e3d0c5e0 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 17c7426dc547..654b3b62d31d 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 75c699d7299a..8e56475575f4 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 f249b2c10c1d..63d2619dc3a4 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 36d1eb799c19..90cd53341d83 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 990d7a7dfbc4..ba44376bdc33 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 0328ce73809c..2094b4b99828 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 3e4004f4a807..cb599c34af46 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 1b2ddb3c71c1..7c7ef1fe29fc 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 1f39a49fb277..d46613b99cae 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 bf92d6c88693..40dbdf9c65e9 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 63e38c5aa2c0..ef7efe4640ba 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 e26759892a07..c7367ac58fef 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 fa25277554b7..66b5ee6a92c6 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 076d55025aca..71e4d9a4dabb 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 0b3e9f841ee1..dab57eb61a42 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 906e3957a8c9..ba253891632b 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 6f11bf7e78ea..2028e3457cb3 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 cbbc74beb2d5..303e663d49ef 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 08a38a30e939..c033a1a4ac1d 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 c8bc4541015d..76e86ac218f1 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 c620023ae102..3cf5bb271803 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 02f054e405bb..98fa0caf3127 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 fc8a521ebc5c..7b0326b5e1b4 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 8d03812db920..981bd8ff33a3 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 06942f69672e..b71daa0aac9b 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 b75dd590dbbf..2015f1e991f4 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 9829c11e4a05..95c176bde927 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 0a54083eb8bd..e7111016f817 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 9fe38d3fd611..71493e47d6e2 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 0348570f6390..965165eeae4f 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 c4dcccc53c8d..17e03496d2b4 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 a2d1b2c077f2..62d94edc1089 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 c5c8a853ad06..682c85a90ba1 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 000000000000..66fd0544be27 --- /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 b23f59ffbc86..4de10a9c3264 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 8f7924c3f309..b36986813298 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 a0581699cce7..60a594687f6d 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 c68133dcfe9e..85fb413232c2 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 ea4a0607d1d4..7835cb529d60 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 65ba89d5efb1..78594d82e3ca 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 363a8ea4d4fd..8b0242dc0034 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 ea309271abac..01f5b0d7bf3d 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 5f68a14a3619..0c6365ad5aa1 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/Unit/Model/Plugin/ProductIdentitiesExtenderTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductIdentitiesExtenderTest.php index 6e3c4220b7d6..f766ec514082 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/Plugin/ProductRepositorySaveTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductRepositorySaveTest.php index 07b4a1faf3db..9ec362fc76e6 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductRepositorySaveTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductRepositorySaveTest.php @@ -113,16 +113,13 @@ protected function setUp(): void */ public function testBeforeSaveWhenProductIsSimple(): void { - $this->product->expects(static::once()) + $this->product->expects(static::atMost(1)) ->method('getTypeId') ->willReturn('simple'); - $this->product->expects(static::never()) + $this->product->expects(static::once()) ->method('getExtensionAttributes'); - $this->assertEquals( - $this->product, - $this->plugin->beforeSave($this->productRepository, $this->product)[0] - ); + $this->assertNull($this->plugin->beforeSave($this->productRepository, $this->product)); } /** @@ -150,52 +147,7 @@ public function testBeforeSaveWithoutOptions(): void $this->productAttributeRepository->expects(static::never()) ->method('get'); - $this->assertEquals( - $this->product, - $this->plugin->beforeSave($this->productRepository, $this->product)[0] - ); - } - - /** - * Test saving a configurable product with same set of attribute values - * - * @return void - */ - public function testBeforeSaveWithLinks(): void - { - $this->expectException(InputException::class); - $this->expectExceptionMessage('Products "5" and "4" have the same set of attribute values.'); - $links = [4, 5]; - $this->product->expects(static::once()) - ->method('getTypeId') - ->willReturn(Configurable::TYPE_CODE); - - $this->product->expects(static::once()) - ->method('getExtensionAttributes') - ->willReturn($this->extensionAttributes); - $this->extensionAttributes->expects(static::once()) - ->method('getConfigurableProductOptions') - ->willReturn(null); - $this->extensionAttributes->expects(static::once()) - ->method('getConfigurableProductLinks') - ->willReturn($links); - - $this->productAttributeRepository->expects(static::never()) - ->method('get'); - - $product = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() - ->setMethods(['getData']) - ->getMock(); - - $this->productRepository->expects(static::exactly(2)) - ->method('getById') - ->willReturn($product); - - $product->expects(static::never()) - ->method('getData'); - - $this->plugin->beforeSave($this->productRepository, $this->product); + $this->assertNull($this->plugin->beforeSave($this->productRepository, $this->product)); } /** 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 a2ef98591718..d85d4d765d20 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 000000000000..6c7068db7f38 --- /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 8a9e4e50ad19..d35fe552e6ea 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 4559fd503d03..36041169514c 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 f5c9382af0bc..240fa180fd87 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/adminhtml/web/js/variations/variations.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js index 6e82fd42692f..6b0040280950 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js @@ -130,9 +130,14 @@ define([ * @return {String|Number|Array} */ getProductValue: function (name) { - name = name.split('/').join(']['); + var value; - return $('[name="product[' + name + ']"]:enabled:not(.ignore-validate)', this.productForm).val(); + name = name.split('/').join(']['); + value = $('[name="product[' + name + ']"]:enabled:not(.ignore-validate)', this.productForm).val(); + if (value === undefined) { + value = this.source.get('data.product.' + name); + } + return value; }, /** 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 cbe840c95795..9d19500bf605 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 fa8b669a1bdd..3671738582de 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 0cb0eddf8a24..be0fe41a1296 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 f112fb891350..b9158cc89176 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 795c38d7e611..f84913928139 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 eb36b1323939..0d307c1fe194 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 adef21a2094e..126fd44e024f 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 af915a826582..f49c6c0284d3 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 078c1a4ff562..9fb4fea6c773 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 99e61e8249da..54bb9e78287c 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 000000000000..e608df9db8b2 --- /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 000000000000..eb6e85235857 --- /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 000000000000..0d983ddf4a4e --- /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 000000000000..9c08ecbb1675 --- /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 000000000000..b46225b07eed --- /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 000000000000..801683ba4361 --- /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 000000000000..400c5471d942 --- /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 000000000000..27782c62d796 --- /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/Section/AdminDefaultCookieSettingsSection.xml b/app/code/Magento/Cookie/Test/Mftf/Section/AdminDefaultCookieSettingsSection.xml index 977db4a8bbf7..0e51354583d6 100644 --- a/app/code/Magento/Cookie/Test/Mftf/Section/AdminDefaultCookieSettingsSection.xml +++ b/app/code/Magento/Cookie/Test/Mftf/Section/AdminDefaultCookieSettingsSection.xml @@ -11,5 +11,7 @@ <section name="AdminDefaultCookieSettingsSection"> <element name="DefaultCookieSettingsTab" type="button" selector="#web_cookie-head"/> <element name="DefaultCookieLifetime" type="input" selector="#web_cookie_cookie_lifetime"/> + <element name="DefaultCookieLifetimeSystemValueCheckbox" type="input" selector="#web_cookie_cookie_lifetime_inherit"/> + <element name="Save" type="button" selector="#save"/> </section> </sections> diff --git a/app/code/Magento/Cookie/Test/Mftf/Test/AdminValidateCookieLifetimeTest.xml b/app/code/Magento/Cookie/Test/Mftf/Test/AdminValidateCookieLifetimeTest.xml index 160b5448d513..a0117f76414e 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 0a9fd4c195f0..4032a7480265 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 f5c78614d800..7947966ea9e9 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 a4a11156956d..14d9a599ae01 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 445666301ade..47238153f9ca 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 deb5717ccac4..e5873c809c5b 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 131c6188d0b9..aae2a2a07e3e 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 09e5b9ed8a69..5b91a7930bfa 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 5a7305ca073f..6006f5cf1450 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 39fb926e410d..c839781f5594 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 000000000000..280f84612a6b --- /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 77d00b09d655..2611c4903c47 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 5d03c83b50b9..7414a11e1357 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 43512796a134..8dc00a759c2f 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 91ae96e2656d..684d4337fb6b 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 4e0eb72df3aa..40cf2c0efc0c 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 7c1b918a0528..6600c46d836d 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 77b2fc9f3233..a7b6b9d36f9a 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 9c607be9f217..165233cc6a88 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 ca9bf4dc7afd..12a2f3f4ff2a 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 f6ba387e913b..3a62580b000b 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 f37ae21a9b83..9a98ca4aae5d 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/DefaultRenderer.php b/app/code/Magento/Customer/Block/Address/Renderer/DefaultRenderer.php index 703d9b2d0154..2e5051cc8601 100644 --- a/app/code/Magento/Customer/Block/Address/Renderer/DefaultRenderer.php +++ b/app/code/Magento/Customer/Block/Address/Renderer/DefaultRenderer.php @@ -189,6 +189,9 @@ public function renderArray($addressAttributes, $format = null) $data[$key] = $v; } } + if (in_array($attributeCode, ['prefix','suffix'])) { + $value = __($value); + } $data[$attributeCode] = $value; } } diff --git a/app/code/Magento/Customer/Block/Address/Renderer/RendererInterface.php b/app/code/Magento/Customer/Block/Address/Renderer/RendererInterface.php index cb942b13410e..fa619e60170b 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 0d94a01698b3..698ed0d03390 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 dc445e4b2dd3..0cca291d90b5 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 7e254b322775..2f70d54c9646 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 2f6609486ee7..3254682552d1 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 64fe7c8b6188..d8bbd8cee966 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 726daf69dc58..fe4dfe8bc360 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 b5448fb3c115..8ac0179088da 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 000000000000..e233a5be8a81 --- /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 ebdf0090fe1c..ad72d9e88a18 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 8cbe5c0680bd..5327dba89059 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 d3d3306a49b4..3b9c6527916c 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 2fc6ed4d422f..d215a935545e 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 d616c03be6bd..085b4ab2d3fd 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 c439d4649987..79c8d75b0e5e 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 4c07864f9b95..da70e7e10bd4 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 31d23c9e3694..c7952bd2be8c 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 e735366d0b8b..ddbcb7610f32 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 74eee759b4ab..2f3585b2e9af 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/Helper/View.php b/app/code/Magento/Customer/Helper/View.php index dcd4ae01940a..560abd335d2f 100644 --- a/app/code/Magento/Customer/Helper/View.php +++ b/app/code/Magento/Customer/Helper/View.php @@ -51,7 +51,7 @@ public function getCustomerName(CustomerInterface $customerData) $name = ''; $prefixMetadata = $this->_customerMetadataService->getAttributeMetadata('prefix'); if ($prefixMetadata->isVisible() && $customerData->getPrefix()) { - $name .= $customerData->getPrefix() . ' '; + $name .= __($customerData->getPrefix()) . ' '; } $name .= $customerData->getFirstname(); @@ -65,7 +65,7 @@ public function getCustomerName(CustomerInterface $customerData) $suffixMetadata = $this->_customerMetadataService->getAttributeMetadata('suffix'); if ($suffixMetadata->isVisible() && $customerData->getSuffix()) { - $name .= ' ' . $customerData->getSuffix(); + $name .= ' ' . __($customerData->getSuffix()); } return $this->escaper->escapeHtml($name); diff --git a/app/code/Magento/Customer/Model/AccountConfirmation.php b/app/code/Magento/Customer/Model/AccountConfirmation.php index f5193bc50026..d95308e4fbe2 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 d5689fd2b8c0..83edd36a8545 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 02a05705b57e..8b4f78ab26c7 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 0ec87066d67c..29648f4dbab3 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 @@ -197,7 +199,7 @@ public function getName() { $name = ''; if ($this->_eavConfig->getAttribute('customer_address', 'prefix')->getIsVisible() && $this->getPrefix()) { - $name .= $this->getPrefix() . ' '; + $name .= __($this->getPrefix()) . ' '; } $name .= $this->getFirstname(); $middleName = $this->_eavConfig->getAttribute('customer_address', 'middlename'); @@ -206,7 +208,7 @@ public function getName() } $name .= ' ' . $this->getLastname(); if ($this->_eavConfig->getAttribute('customer_address', 'suffix')->getIsVisible() && $this->getSuffix()) { - $name .= ' ' . $this->getSuffix(); + $name .= ' ' . __($this->getSuffix()); } return $name; } @@ -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 279628bba139..2a010a280ff7 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 1fed9d5b6b54..d29e42c1e03d 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 5d926b47ca44..03219ab1935b 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 c62d72417837..84f09de699e9 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 b68fb20019da..05b20dfa5823 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 b40bdd12fb05..7b9aedf1a632 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 1841b245099a..0014fd3f56ce 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 afdfe0b30095..04c79888c603 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 2ec12654b08b..f7ceb2ce500f 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 11685f6b23ad..c28ee87aaaf6 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 f4418c283285..95db7353758a 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 c851836134b6..8c7f3e1661fc 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 0f421c1c677c..f05c0948ac07 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 a4f85a9c4a0c..a71cf79a4f51 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 8e64fba4a9b0..7ddf642c9dec 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 54b8b75c9ca3..05788dcaf763 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/Options.php b/app/code/Magento/Customer/Model/Options.php index ec995a12e2bc..c407cd616b6d 100644 --- a/app/code/Magento/Customer/Model/Options.php +++ b/app/code/Magento/Customer/Model/Options.php @@ -100,7 +100,7 @@ private function prepareNamePrefixSuffixOptions($options, $isOptional = false) $options = explode(';', trim($options)); foreach ($options as $value) { - $result[] = $this->escaper->escapeHtml(trim($value)) ?: ' '; + $result[] = $this->escaper->escapeHtml(trim(__($value))) ?: ' '; } if ($isOptional && trim(current($options))) { 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 000000000000..ec837d973759 --- /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 db694ad3295c..ab3cf8cb7d85 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 b9765d7a394f..c2db95813fe9 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 c7b44288bc85..ffb5f41a4068 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 c065f85aa648..0ed677c60c7a 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 292f41e241e0..99720afc9829 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 6e93210d04c3..c7ac2817d741 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 f264245b30c4..9a8135bbf1d9 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 712ba02d5935..c3a43fb14bf9 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 d0115dbee72b..06deaa56a5b6 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 ec2d90c4a7db..6e69681a845f 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 165c411a4633..4b6630c0e7a3 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 000000000000..5b5c8ce1fc0c --- /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 000000000000..63551ff5a757 --- /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 f5667078a379..f63b7f063327 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 000000000000..eae1c83e9f3c --- /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 000000000000..c95cdce2d20f --- /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 000000000000..b3649b0546c4 --- /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 000000000000..afa6b44f6ee6 --- /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 000000000000..a71d0767835b --- /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 000000000000..49ef1ad9cede --- /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 000000000000..219bc9f4482a --- /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 000000000000..074b93d860f1 --- /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 000000000000..d31bacf807ec --- /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 000000000000..567122c45317 --- /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 000000000000..a08d6c706984 --- /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 000000000000..f08a2422e023 --- /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 000000000000..245ed2c8ca46 --- /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/ActionGroup/StorefrontVerifyCustomerDefaultCookieExpiryDateActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontVerifyCustomerDefaultCookieExpiryDateActionGroup.xml new file mode 100644 index 000000000000..f5c40e526141 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontVerifyCustomerDefaultCookieExpiryDateActionGroup.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="StorefrontVerifyCustomerDefaultCookieExpiryDateActionGroup"> + <annotations> + <description>Verify a customer's cookies expiry date on browser's local storage in storefront</description> + </annotations> + <arguments> + <argument name="timezoneOffset" type="string" defaultValue="0"/> + <argument name="timeUnit" type="string" defaultValue="minute"/> + </arguments> + + <!--Verify that there are cookies exists with the given name `section_data_ids`, `mage-cache-sessid`, `mage-cache-storage`--> + <seeCookie userInput="section_data_ids" stepKey="seeCookieForMagentoSectionDataIds"/> + <seeCookie userInput="mage-cache-sessid" stepKey="seeCookieForMagentoCacheSessionId"/> + <seeCookie userInput="mage-cache-storage" stepKey="seeCookieForMagentoCacheStorage"/> + + <!--Grab the cookies attribute with the given names `section_data_ids`, `mage-cache-sessid`, `mage-cache-storage--> + <grabCookieAttributes userInput="section_data_ids" stepKey="grabCookieForMagentoDataIds"/> + <grabCookieAttributes userInput="mage-cache-sessid" stepKey="grabCookieForMagentoCacheSessionId"/> + <grabCookieAttributes userInput="mage-cache-storage" stepKey="grabCookieForMagentoCacheStorage"/> + + <!--Grab expected date--> + <generateDate date="{{timezoneOffset}} {{timeUnit}}" format="d/m/Y" timezone="UTC" stepKey="generateExpireDate"/> + + <!--Assert cookies `section_data_ids`, `mage-cache-sessid`, `mage-cache-storage` having expiry date equal to expected date--> + <assertEquals stepKey="validateExpiryDateForMagentoDataIds"> + <actualResult type="string">{{$grabCookieForMagentoDataIds['expiry']}}</actualResult> + <expectedResult type="string">{{$generateExpireDate}}</expectedResult> + </assertEquals> + <assertEquals stepKey="validateExpiryDateForMagentoCacheSessionId"> + <actualResult type="string">{{$grabCookieForMagentoCacheSessionId['expiry']}}</actualResult> + <expectedResult type="string">{{$generateExpireDate}}</expectedResult> + </assertEquals> + <assertEquals stepKey="validateExpiryDateForMagentoCacheStorage"> + <actualResult type="string">{{$grabCookieForMagentoCacheStorage['expiry']}}</actualResult> + <expectedResult type="string">{{$generateExpireDate}}</expectedResult> + </assertEquals> + </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 d47409cb0953..c6eb85aacfb2 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 354ff72f62c4..47b37a4a8e26 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 b4814a3e4bed..8f4f0a1596a8 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 1752cb6d04c3..fd1fc32996ae 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 b3a015113549..27944a6ae6d3 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 791ac991bb8c..a8246586fd3c 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 099c8da06552..2ad315fa840e 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 000000000000..8132a785bf53 --- /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 5497ed9950a6..3f8acf6f1785 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 9806bb036813..b1eeeec6de62 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 d6b586e42f28..ba8159948701 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 e213185f28f2..58395ca8e3d7 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 9a13eb38dd61..cf25e974d3a0 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 e0736895221d..31f9d47ce4b2 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 b7096625aca8..4bec062a244a 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 205da22833cc..233f4fd94ea2 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 543f26a1aaf6..b4e21e7cba02 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 6c57fd4dfb4b..b88faaa63904 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 a1595cfebb9f..08211057099a 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 78adcd9058ec..6c8dce091b08 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 7bc01cab564c..f4e4590f5301 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 631349cb6196..2ae82e919784 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 cffa1bc95ac6..7ce5e0f0e105 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 29941d7223c0..fa6af71860fa 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 7f66b657180f..682ff3e75152 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 8f2e20e90d75..11a71b44883d 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 2cd231d0bf39..e5784c1a72d0 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 6d917d3d18b4..b3c73b3d08b0 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 c3dbad670815..cd34f8281a06 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 e8198cb79262..04636d5614a5 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 3416c64a7e9d..640f068ad8e5 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 4548efe07196..35be6c636d67 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 000000000000..cd339c0cbf45 --- /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 d54977b2e2ab..05b7f01d4a39 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 207430c7bc7b..8d3c6c50d055 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 bc0c3e00d75a..952dc38fca84 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 eb10a9bf469c..080b933cdcfd 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 8d5535a48f8a..7303d2b083b2 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 fe4a3ea39313..b42efec9f00b 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 5ba49cbcefba..ebe8cb3da823 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 059216036280..af9a9db5809e 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 5683a75f2a38..d884b11ece74 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 000000000000..b6aeb49a4e65 --- /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 cffa34ec2af6..aea21bee38d5 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 f8f3dfe19d6e..49663857fa45 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 7f1b1dfee7ce..508d64cf18e9 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 ef4dc560d4fe..9655303856a4 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 5f20eb9cd5e6..21b21ad772c9 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 7217f452e83f..a1b091cea375 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 4c4175bb3219..7d6515d8b6bf 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 e1cd7146856d..2f7172692d27 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 5206f0e14efa..6470edcfa0e4 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 4d833cce920e..31e2aab8e25d 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 a273d9e7431d..eab6140adae0 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 3fa29aef9908..ac76abf829b6 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 c6e370fb6a76..847e144286c1 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 d81d7da6b5b0..9be754b34e10 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 09ff169b1fac..d2655100c404 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 7a68f48d2ab9..1a1185a083f3 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 c990c9ff659a..0c8940608d91 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 04bdc4e6a608..8b88f0cd7ae3 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 c4bcc4e3854d..8e211a8f52d2 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 ff60ac92853c..977d00696d3d 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 000db5d79f76..7409b573f183 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 e32ae04495fe..b907f85109c4 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 368a5c8db7d3..cee34fb258aa 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 c98d20a32ba1..898dcd9d79f8 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 dd982077ccb6..3a0056f8adb4 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 d4f851ee21c2..824cc97b2e11 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 cec7f8460de5..d5fa3255833c 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 c3c8bd5d7c40..a138b3f38d2e 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 ccca330f5ff1..326da5e390ea 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 d9349dae2932..c90934d256e7 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 fe7a54bb2355..08be552f3819 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 a71d4944617a..6410dcd7df42 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 d04e60ef86bb..50267bc1bf9e 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 5834772a41fa..2da490002fc2 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 0d64ceb54583..f0f47e416ea2 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 d9e665ec7a2a..ad1b6d0fb74e 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 ef610831a721..490b85c4176d 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 07ac295e5cce..8e977fbf6857 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 ba113c739d70..1a9930b2b0ef 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 4309076df214..b4e1d8134199 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 716b2b2bab9c..059c8c78126e 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 faf03ad666bd..510f3979f8cc 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 2b0da367a9ec..366d1805705b 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 62bab5669307..95981aa11418 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 51efd4e23f5d..d73a15cd6b10 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 c69c4dd071e3..d0df6a65d1ab 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 7d7218c59d14..833386fa7116 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 4e967cdee8df..6c312f90cfec 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 a7dc3c7fde7f..d6535e606f37 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 7845d3cee44e..77a2330a554b 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 f148761f1b97..9a0ca76f761a 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 d91cb9f15852..ecc89bdadfca 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 new file mode 100644 index 000000000000..f52f18bcc44e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontRetainLocalCacheStorageTest.xml @@ -0,0 +1,99 @@ +<?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="StorefrontRetainLocalCacheStorageTest"> + <annotations> + <features value="Customer"/> + <stories value="Local cache storage is not retained for the expected period."/> + <title value="Verify that Local cache storage is retained for the expected period."/> + <description value="Verify that Local cache storage is retained for the expected period."/> + <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--> + <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"> + <argument name="Customer" value="$$createCustomer$$"/> + </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"> + <argument name="timezoneOffset" value="{$getTimezoneOffset}"/> + </actionGroup> + + <!--Logout customer before in case of it logged in from previous test--> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> + + <!--Login as admin--> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> + + <!--Clear browser locale storage for magento site--> + <resetCookie userInput="section_data_ids" stepKey="resetCookieForMagentoCacheSectionDataIds"/> + <resetCookie userInput="mage-cache-sessid" stepKey="resetCookieForMagentoCacheSessionId"/> + <resetCookie userInput="mage-cache-storage" stepKey="resetCookieForMagentoCacheStorage"/> + + <!--Set-Cookie Lifetime to 30 days (2592000) under Stores > Configuration > General > Web > Default Cookie Settings--> + <actionGroup ref="AdminNavigateToDefaultCookieSettingsActionGroup" stepKey="goToCurrencySetupPage"/> + <!--Ensure the checkbox `use system value` is unchecked.--> + <uncheckOption selector="{{AdminDefaultCookieSettingsSection.DefaultCookieLifetimeSystemValueCheckbox}}" stepKey="uncheckCheckboxForSystemValue"/> + <fillField userInput="2592000" selector="{{AdminDefaultCookieSettingsSection.DefaultCookieLifetime}}" stepKey="fillDefaultLabel"/> + <click selector="{{AdminDefaultCookieSettingsSection.Save}}" stepKey="clickSaveConfig"/> + + <!--Clear cache and perform reindex--> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value=""/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + + <!--Login storefront again using registered customer credentials--> + <actionGroup ref="StorefrontOpenCustomerLoginPageActionGroup" stepKey="goToSignInPage"/> + <actionGroup ref="StorefrontFillCustomerLoginFormActionGroup" stepKey="fillLoginFormWithCustomerData"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonFirstAttempt"/> + + <!--Grab current timezone offset after 30 days--> + <executeJS function="return {$getTimezoneOffset} + (30*24*60);" stepKey="getTimezoneOffsetAfterReset"/> + <actionGroup ref="StorefrontVerifyCustomerDefaultCookieExpiryDateActionGroup" stepKey="VerifyCookiesExpiryDateAfterReset"> + <argument name="timezoneOffset" value="{$getTimezoneOffsetAfterReset}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml index 1f92b429603e..723f8b62b998 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 d41b1cf86da5..b77b0b731836 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 438e875d9374..f7f029b3e375 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 0539b50dcaac..4bba032ad561 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 7b5ad9d70fd7..78ecb05eceeb 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 9e5be5abe95a..e0685bb46fe7 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 1f2c07c325c1..f4612b58253e 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 c977334c5f85..eb58b3021b86 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 000000000000..f4b8198c097c --- /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/Unit/Block/Adminhtml/Edit/Tab/NewsletterTest.php b/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/NewsletterTest.php index 979947047253..1915f1723849 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 19db3d8317da..f30fd7facebb 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 928ec5960a2b..9fcbbf77a3b8 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 fab560aaa21b..c09a25f2cd7f 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 000000000000..074d40021a18 --- /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 9e68d53fd594..4a2cf47834d7 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 cee6a8aefd1a..88f5289645af 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 b97cff96bfbc..99890019e415 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 b342d15885e5..9f345f3c1de1 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 3613cd1990ee..a65f95cd28d6 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 35f9b0b8371c..c7ae84b1fa0c 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 c9fbf0e6bd2f..77c1302b780c 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 0be021265205..f54fcc2f77af 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 000000000000..107df2c2863e --- /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 000000000000..72d5f36e2266 --- /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 08fd76afb76d..7136ebf9b5ef 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 3be200bdf90b..b922a52478dc 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 000000000000..b84e78ca3591 --- /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 000000000000..bf259040aaf9 --- /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 e802505caf9d..9ad800ae14fc 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 459ac3e29e99..954293f58dc2 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 79602c031f2e..5202ef1f479f 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 8fa8718fe37e..000000000000 --- 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 000000000000..e8c9210d32e1 --- /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 000000000000..c7a7be29a294 --- /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 4a68227dd27b..000000000000 --- 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 75349043e8ba..000000000000 --- 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 ef2047644759..39c82c20f2ec 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 569f9d09c208..ec76e09fdf45 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 22596e0b901b..23a7c9ebb403 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 b178f51f8919..04dee8d9f6b6 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 31f3e11522e1..827a153e9467 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 18627b68320e..c5d7a28a3651 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 0787e0713aa9..b9808747c6c7 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 755a8e6df3db..3d1132332d70 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 c75086e8ea49..0afe06becc53 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 e89aa5ab624d..3dd38d61aee0 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 7c8a6991e5a8..7fcf612de0c0 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 8fb51eeb6650..90cd080cf2f6 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 b431373ca412..11285070e002 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 6734e9ad30a4..342f1ea23cdf 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 2c6615828394..1455fdbbd9f1 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 0cc3dd5973b2..daca557450c4 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 900be3d20bf2..58af2f1bf594 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 eb50ea645478..a1df853cc71b 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/templates/widget/name.phtml b/app/code/Magento/Customer/view/frontend/templates/widget/name.phtml index 00c1f124bd26..c72566554772 100644 --- a/app/code/Magento/Customer/view/frontend/templates/widget/name.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/widget/name.phtml @@ -24,31 +24,40 @@ $prefix = $block->showPrefix(); $middle = $block->showMiddlename(); $suffix = $block->showSuffix(); ?> -<?php if (($prefix || $middle || $suffix) && !$block->getNoWrap()) : ?> +<?php if (($prefix || $middle || $suffix) && !$block->getNoWrap()): ?> <div class="field required fullname <?= $block->escapeHtmlAttr($block->getContainerClassName()) ?>"> - <label for="<?= $block->escapeHtmlAttr($block->getFieldId('firstname')) ?>" class="label"><span><?= $block->escapeHtml(__('Name')) ?></span></label> + <label for="<?= $block->escapeHtmlAttr($block->getFieldId('firstname')) ?>" class="label"> + <span><?= $block->escapeHtml(__('Name')) ?></span> + </label> <div class="control"> <fieldset class="fieldset fieldset-fullname"> <div class="fields"> <?php endif; ?> - <?php if ($prefix) : ?> + <?php if ($prefix): ?> <div class="field field-name-prefix<?= $block->isPrefixRequired() ? ' required' : '' ?>"> - <label class="label" for="<?= $block->escapeHtmlAttr($block->getFieldId('prefix')) ?>"><span><?= $block->escapeHtml($block->getStoreLabel('prefix')) ?></span></label> + <label class="label" for="<?= $block->escapeHtmlAttr($block->getFieldId('prefix')) ?>"> + <span><?= $block->escapeHtml($block->getStoreLabel('prefix')) ?></span> + </label> <div class="control"> - <?php if ($block->getPrefixOptions() === false) : ?> + <?php if ($block->getPrefixOptions() === false): ?> <input type="text" id="<?= $block->escapeHtmlAttr($block->getFieldId('prefix')) ?>" name="<?= $block->escapeHtmlAttr($block->getFieldName('prefix')) ?>" value="<?= $block->escapeHtmlAttr($block->getObject()->getPrefix()) ?>" title="<?= $block->escapeHtmlAttr($block->getStoreLabel('prefix')) ?>" - class="input-text <?= $block->escapeHtmlAttr($block->getAttributeValidationClass('prefix')) ?>" <?= $block->isPrefixRequired() ? ' data-validate="{required:true}"' : '' ?>> - <?php else : ?> + class="input-text + <?= $block->escapeHtmlAttr($block->getAttributeValidationClass('prefix')) ?>" + <?= $block->isPrefixRequired() ? ' data-validate="{required:true}"' : '' ?>> + <?php else: ?> <select id="<?= $block->escapeHtmlAttr($block->getFieldId('prefix')) ?>" name="<?= $block->escapeHtmlAttr($block->getFieldName('prefix')) ?>" title="<?= $block->escapeHtmlAttr($block->getStoreLabel('prefix')) ?>" - class="<?= $block->escapeHtmlAttr($block->getAttributeValidationClass('prefix')) ?>" <?= $block->isPrefixRequired() ? ' data-validate="{required:true}"' : '' ?> > - <?php foreach ($block->getPrefixOptions() as $_option) : ?> - <option value="<?= $block->escapeHtmlAttr($_option) ?>"<?php if ($block->getObject()->getPrefix() == $_option) : ?> selected="selected"<?php endif; ?>> + class="<?= $block->escapeHtmlAttr($block->getAttributeValidationClass('prefix')) ?>" + <?= $block->isPrefixRequired() ? ' data-validate="{required:true}"' : '' ?> > + <?php foreach ($block->getPrefixOptions() as $_option): ?> + <option value="<?= $block->escapeHtmlAttr(__($_option)) ?>" + <?php if ($block->getObject()->getPrefix() == $_option): ?> + selected="selected"<?php endif; ?>> <?= $block->escapeHtml(__($_option)) ?> </option> <?php endforeach; ?> @@ -58,55 +67,76 @@ $suffix = $block->showSuffix(); </div> <?php endif; ?> <div class="field field-name-firstname required"> - <label class="label" for="<?= $block->escapeHtmlAttr($block->getFieldId('firstname')) ?>"><span><?= $block->escapeHtml($block->getStoreLabel('firstname')) ?></span></label> + <label class="label" for="<?= $block->escapeHtmlAttr($block->getFieldId('firstname')) ?>"> + <span><?= $block->escapeHtml($block->getStoreLabel('firstname')) ?></span> + </label> <div class="control"> <input type="text" id="<?= $block->escapeHtmlAttr($block->getFieldId('firstname')) ?>" name="<?= $block->escapeHtmlAttr($block->getFieldName('firstname')) ?>" value="<?= $block->escapeHtmlAttr($block->getObject()->getFirstname()) ?>" title="<?= $block->escapeHtmlAttr($block->getStoreLabel('firstname')) ?>" - class="input-text <?= $block->escapeHtmlAttr($block->getAttributeValidationClass('firstname')) ?>" <?= ($block->getAttributeValidationClass('firstname') == 'required-entry') ? ' data-validate="{required:true}"' : '' ?>> + class="input-text + <?= $block->escapeHtmlAttr($block->getAttributeValidationClass('firstname')) ?>" + <?= ($block->getAttributeValidationClass('firstname') == 'required-entry') ? ' + data-validate="{required:true}"' : '' ?>> </div> </div> - <?php if ($middle) : ?> + <?php if ($middle): ?> <?php $isMiddlenameRequired = $block->isMiddlenameRequired(); ?> <div class="field field-name-middlename<?= $isMiddlenameRequired ? ' required' : '' ?>"> - <label class="label" for="<?= $block->escapeHtmlAttr($block->getFieldId('middlename')) ?>"><span><?= $block->escapeHtml($block->getStoreLabel('middlename')) ?></span></label> + <label class="label" for="<?= $block->escapeHtmlAttr($block->getFieldId('middlename')) ?>"> + <span><?= $block->escapeHtml($block->getStoreLabel('middlename')) ?></span> + </label> <div class="control"> <input type="text" id="<?= $block->escapeHtmlAttr($block->getFieldId('middlename')) ?>" name="<?= $block->escapeHtmlAttr($block->getFieldName('middlename')) ?>" value="<?= $block->escapeHtmlAttr($block->getObject()->getMiddlename()) ?>" title="<?= $block->escapeHtmlAttr($block->getStoreLabel('middlename')) ?>" - class="input-text <?= $block->escapeHtmlAttr($block->getAttributeValidationClass('middlename')) ?>" <?= $isMiddlenameRequired ? ' data-validate="{required:true}"' : '' ?>> + class="input-text + <?= $block->escapeHtmlAttr($block->getAttributeValidationClass('middlename')) ?>" + <?= $isMiddlenameRequired ? ' data-validate="{required:true}"' : '' ?>> </div> </div> <?php endif; ?> <div class="field field-name-lastname required"> - <label class="label" for="<?= $block->escapeHtmlAttr($block->getFieldId('lastname')) ?>"><span><?= $block->escapeHtml($block->getStoreLabel('lastname')) ?></span></label> + <label class="label" for="<?= $block->escapeHtmlAttr($block->getFieldId('lastname')) ?>"> + <span><?= $block->escapeHtml($block->getStoreLabel('lastname')) ?></span> + </label> <div class="control"> <input type="text" id="<?= $block->escapeHtmlAttr($block->getFieldId('lastname')) ?>" name="<?= $block->escapeHtmlAttr($block->getFieldName('lastname')) ?>" value="<?= $block->escapeHtmlAttr($block->getObject()->getLastname()) ?>" title="<?= $block->escapeHtmlAttr($block->getStoreLabel('lastname')) ?>" - class="input-text <?= $block->escapeHtmlAttr($block->getAttributeValidationClass('lastname')) ?>" <?= ($block->getAttributeValidationClass('lastname') == 'required-entry') ? ' data-validate="{required:true}"' : '' ?>> + class="input-text + <?= $block->escapeHtmlAttr($block->getAttributeValidationClass('lastname')) ?>" + <?= ($block->getAttributeValidationClass('lastname') == 'required-entry') ? ' + data-validate="{required:true}"' : '' ?>> </div> </div> - <?php if ($suffix) : ?> + <?php if ($suffix): ?> <div class="field field-name-suffix<?= $block->isSuffixRequired() ? ' required' : '' ?>"> - <label class="label" for="<?= $block->escapeHtmlAttr($block->getFieldId('suffix')) ?>"><span><?= $block->escapeHtml($block->getStoreLabel('suffix')) ?></span></label> + <label class="label" for="<?= $block->escapeHtmlAttr($block->getFieldId('suffix')) ?>"> + <span><?= $block->escapeHtml($block->getStoreLabel('suffix')) ?></span> + </label> <div class="control"> - <?php if ($block->getSuffixOptions() === false) : ?> + <?php if ($block->getSuffixOptions() === false): ?> <input type="text" id="<?= $block->escapeHtmlAttr($block->getFieldId('suffix')) ?>" name="<?= $block->escapeHtmlAttr($block->getFieldName('suffix')) ?>" value="<?= $block->escapeHtmlAttr($block->getObject()->getSuffix()) ?>" title="<?= $block->escapeHtmlAttr($block->getStoreLabel('suffix')) ?>" - class="input-text <?= $block->escapeHtmlAttr($block->getAttributeValidationClass('suffix')) ?>" <?= $block->isSuffixRequired() ? ' data-validate="{required:true}"' : '' ?>> - <?php else : ?> + class="input-text + <?= $block->escapeHtmlAttr($block->getAttributeValidationClass('suffix')) ?>" + <?= $block->isSuffixRequired() ? ' data-validate="{required:true}"' : '' ?>> + <?php else: ?> <select id="<?= $block->escapeHtmlAttr($block->getFieldId('suffix')) ?>" name="<?= $block->escapeHtmlAttr($block->getFieldName('suffix')) ?>" title="<?= $block->escapeHtmlAttr($block->getStoreLabel('suffix')) ?>" - class="<?= $block->escapeHtmlAttr($block->getAttributeValidationClass('suffix')) ?>" <?= $block->isSuffixRequired() ? ' data-validate="{required:true}"' : '' ?>> - <?php foreach ($block->getSuffixOptions() as $_option) : ?> - <option value="<?= $block->escapeHtmlAttr($_option) ?>"<?php if ($block->getObject()->getSuffix() == $_option) : ?> selected="selected"<?php endif; ?>> + class="<?= $block->escapeHtmlAttr($block->getAttributeValidationClass('suffix')) ?>" + <?= $block->isSuffixRequired() ? ' data-validate="{required:true}"' : '' ?>> + <?php foreach ($block->getSuffixOptions() as $_option): ?> + <option value="<?= $block->escapeHtmlAttr(__($_option)) ?>" + <?php if ($block->getObject()->getSuffix() == $_option): ?> + selected="selected"<?php endif; ?>> <?= $block->escapeHtml(__($_option)) ?> </option> <?php endforeach; ?> @@ -116,7 +146,7 @@ $suffix = $block->showSuffix(); </div> <?php endif; ?> - <?php if (($prefix || $middle || $suffix) && !$block->getNoWrap()) : ?> + <?php if (($prefix || $middle || $suffix) && !$block->getNoWrap()): ?> </div> </fieldset> </div> 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 213aa105ba25..5ff83bbb9b14 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 f96ae0dee861..046f69ca6e25 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 dc6aef3df790..ee0010e821d1 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 37ac79472bb2..153379cd9767 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 dba15882434d..28d777e27cb0 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 b3ae57e0ff99..0140bcd3739c 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 5a302f4c3df2..6bb4a8118252 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 a631b7ba8619..971b0352b893 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 c62a93180964..01bb007ef618 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 000000000000..98a38dab4b13 --- /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 000000000000..dc46f08fd043 --- /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 000000000000..8724e57ff11c --- /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 d82b8c6f941f..8ff74178f2be 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 000000000000..bbc8b2eaab04 --- /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 000000000000..03332301706c --- /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 000000000000..db67d2e860c4 --- /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 000000000000..4b4c187bbd94 --- /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 000000000000..85f659cc0adc --- /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 000000000000..f1d6406295c5 --- /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 000000000000..f5e10440dddd --- /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 000000000000..7b953b253451 --- /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 000000000000..75493acba88a --- /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 000000000000..33333eb8cf68 --- /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 000000000000..5463c90894ef --- /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 000000000000..a8207232b177 --- /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 000000000000..2030c24fb184 --- /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 a6b6ad71109c..9cf858da9c67 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 000000000000..7850134e45f3 --- /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 000000000000..187e54821307 --- /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 8cdf6518a4ef..4d6c199dab76 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 a2b8c3fa78a2..af6a8bf671a7 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 b16ce7ee710a..e39ae2ba17db 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 f632f52b3584..8f5df3db3b64 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 4699af2b9c38..4854efb5a1cd 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 5967d2e9f8ac..9fb9668de0e7 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 000000000000..6fbc99607908 --- /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 1e616e37a12f..7aeb9ca1bee6 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 b15df7fc0be6..bdbbaa3e7f43 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 439ce4742ca3..e7e9a1484bb2 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 4ba93557f854..17a2b3678d9c 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 0249985f27e8..2a7eb9b4319a 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 2e7a915d1b5a..50c978eae1a7 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 2d8c105d2b29..10e3b5efcbdb 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 59a2f0f7cfe9..f72630c954e8 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 0e4bdb11e0bb..1d55d55b54e3 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 3a7d3663c887..8c3e0f750deb 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 bf7014cdbb49..82ba4102736f 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 b752eaa111fa..1525637c017a 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 f0ccdb7217ec..aa29586df140 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 e64c7f1ae377..beab9665df92 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 0b9096659ac3..000000000000 --- 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 5e67043f3f0c..80d6a0f8de21 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 8473d0ae426e..d0ad6f15705d 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 bc18e9bbd953..ff8d4514d225 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 a7637b83fa3f..8782e022fe87 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 f84de7c3593f..245b8a228c27 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 000000000000..23eb9eaa4c2f --- /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/AddDataForCzechia.php b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForCzechia.php new file mode 100644 index 000000000000..68b189d882ef --- /dev/null +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForCzechia.php @@ -0,0 +1,99 @@ +<?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 Czech Republic States + */ +class AddDataForCzechia 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->getDataForCzechia() + ); + + return $this; + } + + /** + * Czechia states data. + * + * @return array + */ + private function getDataForCzechia() + { + return [ + ['CZ', 'CZ-10', 'Praha, Hlavní město'], + ['CZ', 'CZ-20', 'Středočeský kraj'], + ['CZ', 'CZ-31', 'Jihočeský kraj'], + ['CZ', 'CZ-32', 'Plzeňský kraj'], + ['CZ', 'CZ-41', 'Karlovarský kraj'], + ['CZ', 'CZ-42', 'Ústecký kraj'], + ['CZ', 'CZ-51', 'Liberecký kraj'], + ['CZ', 'CZ-52', 'Královéhradecký kraj'], + ['CZ', 'CZ-53', 'Pardubický kraj'], + ['CZ', 'CZ-63', 'Kraj Vysočina'], + ['CZ', 'CZ-64', 'Jihomoravský kraj'], + ['CZ', 'CZ-71', 'Olomoucký kraj'], + ['CZ', 'CZ-72', 'Zlínský kraj'], + ['CZ', 'CZ-80', 'Moravskoslezský kraj'], + ]; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + InitializeDirectoryData::class, + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} 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 000000000000..74720e7b931a --- /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/view/frontend/web/js/region-updater.js b/app/code/Magento/Directory/view/frontend/web/js/region-updater.js index 9d22cf90f258..e6e08fddacda 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 000000000000..16ba9dcab769 --- /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 000000000000..cbadda1b2e45 --- /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 000000000000..bc905b57b622 --- /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 000000000000..a5eed66cb3f4 --- /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 000000000000..d990fbc681d9 --- /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 000000000000..19b8495c66b6 --- /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 ed16732f3dcc..b5176b88ee20 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 475e8ebfbd76..7155adeadaf0 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 9351568c5a75..65be8e0ed9e7 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 1760f2228bf0..ce2f7dd577ef 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 608b54d997f3..76aaf748f7e5 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 bfa0c77280f4..9e3cb81d4258 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 b97ff42fc22f..7ecb79f8dab9 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 d291f221da5b..20661f5966c8 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 284e559c6823..2d152098a28d 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 bfa53f9beb4f..089878f7e7a0 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 4ba8ef1b7fe2..93172dc84530 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/Unit/Observer/SaveDownloadableOrderItemObserverTest.php b/app/code/Magento/Downloadable/Test/Unit/Observer/SaveDownloadableOrderItemObserverTest.php index 09edbf4935fe..b28fec61b125 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 8ded865057dc..10e7ddbb86c2 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 3be1094f7a4b..7c3c30482fd8 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 91e290dbbcdf..afe304ff02ac 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 03c2ae36b9cd..57b7104c4be1 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 b3a2063af340..62c4318a3bb6 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 b59a84675a0c..5bb48c7b6f7e 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 f86eddeed3ea..487729323556 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 0d4456da48db..b8eaa74e40ff 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/Eav/Api/AttributeSetRepositoryInterface.php b/app/code/Magento/Eav/Api/AttributeSetRepositoryInterface.php index be4b5a3fa0fe..224fde25460c 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 40f9a4ae4e93..769957711321 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 22cac884491a..d10c47cd0d4c 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; @@ -97,6 +104,7 @@ public function validateValue($value) * * @param array|string $value * @return $this + * @throws LocalizedException */ public function compactValue($value) { @@ -124,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) { @@ -136,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)); diff --git a/app/code/Magento/Eav/Model/AttributeDataFactory.php b/app/code/Magento/Eav/Model/AttributeDataFactory.php index e5844b093e34..ec2d02c65284 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 000000000000..9317426addad --- /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 a7e49b126f35..161e8f2b2d24 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 08823bacce4d..51b199a9876e 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 e1094a331149..ecdb2f55f4f4 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 af621e17f424..5628787f7deb 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 6f6dc0a47f5a..2c9b6d68b0bb 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 bdd2899b47b6..b9139c6fff0f 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 abd817940956..a12568cbde6c 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 b994d793ed04..c25c7f5ec90e 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 fdc71faa9090..000000000000 --- 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 000000000000..58986de9801c --- /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 000000000000..f2556e5caad4 --- /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 000000000000..6cfacc2328c7 --- /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 7abb54e780f5..897640f852cc 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 9c6adc0354f8..1711d87c2648 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 66404c3ef380..7cc41f4aa94e 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 3e894c5f76a1..b11e88b4e121 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 153735f98837..554896a70409 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 9438178e5608..9912d5191589 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 7b29b9dde679..4175608db5f0 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 6710044ac6c8..f669e534a973 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 000000000000..cc7f66594d5c --- /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 000000000000..4d25bdb03ca8 --- /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 000000000000..f05e5c3f9265 --- /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 11e41d67660f..af67bb74a19e 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 4443e912bbf4..8cb51876aed0 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; @@ -199,6 +200,16 @@ public function alphanumWithSpacesDataProvider(): array ]; } + /** + * Test for string with diacritics validation + */ + public function testValidateValueStringWithDiacritics(): void + { + $inputValue = "á â à å ä ð é ê è ë í î ì ï ó ô ò ø õ ö ú û ù ü æ œ ç ß a ĝ ń ŕ ý ð ñ"; + $expectedResult = true; + self::assertEquals($expectedResult, $this->model->validateValue($inputValue)); + } + /** * @param array $attributeData * @return Attribute @@ -213,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 9a472b6b2aec..6d3c1b24b2f4 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 7b554a19fc28..278bcfdaad44 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 88daf1a8a6f5..e1aab7f44b48 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 ed5ff1394234..33b4b906b202 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 000000000000..5c918a235480 --- /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 000000000000..96d5bd6b547b --- /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 000000000000..288e0a5cce8e --- /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 000000000000..4f9b6f63f315 --- /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 000000000000..a19ff9c9b8bd --- /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 000000000000..c85054386d15 --- /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 000000000000..67b2692e5579 --- /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 000000000000..7be0ae4b7385 --- /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 000000000000..30fdc0dba5ab --- /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 000000000000..2a586c824efa --- /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 000000000000..56903cda7a14 --- /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 000000000000..65a4124fb6ef --- /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 000000000000..91eb721b4d82 --- /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 000000000000..fb7cc4134aa8 --- /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 000000000000..b4230ea60c95 --- /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 000000000000..c84a7bdd5134 --- /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 000000000000..257c7c00da58 --- /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 000000000000..91c00a7ad69c --- /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 000000000000..f4f9853c1b6a --- /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 000000000000..d02542e54d33 --- /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 000000000000..25df607cfbde --- /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 000000000000..2b67b43d1e9a --- /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 0ebeca292975..df4761c22101 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 000000000000..0d852b401eac --- /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 000000000000..3b402a6e0dfb --- /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 000000000000..c4fd8403bd67 --- /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 000000000000..0390f3aaf99a --- /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 000000000000..5a436b911ed0 --- /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 000000000000..bafcd4a11f49 --- /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 000000000000..dd713f4f69ac --- /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 ba1dc948c808..be4879ac18ce 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 000000000000..30c4e7225851 --- /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 000000000000..93726a50a9c0 --- /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 25f53c4ad7ea..6878b8c277a7 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 41a5edc900af..000000000000 --- 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 dd9a9d904ddf..3258658f6d76 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 58f7029cd16e..b4db015ff680 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 000000000000..6066d6a2172e --- /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 954deaec639e..d4bbfa4e940b 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 000000000000..aacbdc335f4c --- /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 fed36ff6b1c8..761113ff2988 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 bbfcce6aa695..632f82ea80cc 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 7ac6588b8786..6baed881aac2 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 9a556460426f..b7a04eb69008 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 840a4e16e8ab..fc665ec35a72 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 1371e8eb1cca..2ed39e58a3aa 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 d77652c616c5..b61df3ca0480 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 c1170a14d697..6f18a2d45d83 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 000000000000..a260928233e7 --- /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 59fdd2c25767..28a15ecdf72a 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 26173fcf29b0..000000000000 --- 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 8576d8df0cc9..000000000000 --- 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 2560d7e26e7d..000000000000 --- 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 9fcd573600f1..000000000000 --- 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 a9fb67f209aa..c1772086d7ba 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 b5151bbd578c..47d031e2954c 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; @@ -19,9 +20,11 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Stdlib\ArrayManager; use Psr\Log\LoggerInterface; +use Magento\AdvancedSearch\Helper\Data; /** * Elasticsearch adapter + * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Elasticsearch @@ -110,6 +113,11 @@ class Elasticsearch */ private $arrayManager; + /** + * @var Data + */ + protected $helper; + /** * @var array */ @@ -117,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 @@ -125,6 +143,7 @@ class Elasticsearch * @param LoggerInterface $logger * @param Index\IndexNameResolver $indexNameResolver * @param BatchDataMapperInterface $batchDocumentDataMapper + * @param Data $helper * @param array $options * @param ProductAttributeRepositoryInterface|null $productAttributeRepository * @param StaticField|null $staticFieldProvider @@ -141,6 +160,7 @@ public function __construct( LoggerInterface $logger, IndexNameResolver $indexNameResolver, BatchDataMapperInterface $batchDocumentDataMapper, + Data $helper, $options = [], ProductAttributeRepositoryInterface $productAttributeRepository = null, StaticField $staticFieldProvider = null, @@ -154,6 +174,7 @@ public function __construct( $this->logger = $logger; $this->indexNameResolver = $indexNameResolver; $this->batchDocumentDataMapper = $batchDocumentDataMapper; + $this->helper = $helper; $this->productAttributeRepository = $productAttributeRepository ?: ObjectManager::getInstance()->get(ProductAttributeRepositoryInterface::class); $this->staticFieldProvider = $staticFieldProvider ?: @@ -172,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 * @@ -224,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; @@ -299,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; @@ -329,18 +419,30 @@ protected function getDocsArrayInBulkIndexFormat( ]; foreach ($documents as $id => $document) { - $bulkArray['body'][] = [ - $action => [ - '_id' => $id, - '_type' => $this->clientConfig->getEntityType(), - '_index' => $indexName - ] - ]; + if ($this->helper->isClientOpenSearchV2()) { + $bulkArray['body'][] = [ + $action => [ + '_id' => $id, + '_index' => $indexName + ] + ]; + } else { + $bulkArray['body'][] = [ + $action => [ + '_id' => $id, + '_type' => $this->clientConfig->getEntityType(), + '_index' => $indexName + ] + ]; + } if ($action == self::BULK_ACTION_INDEX) { $bulkArray['body'][] = $document; } } + if ($this->helper->isClientOpenSearchV2()) { + unset($bulkArray['type']); + } return $bulkArray; } 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 75636991e7ee..e97a46eed7d3 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 1db0bad36e24..87712641c05d 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 56cdebdfc281..000000000000 --- 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 a84281dcbfb7..bd7eed24fd63 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 5b6103a65314..2355075bf41b 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 6c2322cc5d9d..d7d7fb5ce89d 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 9ccd1471136a..9646cc355148 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 67b12a307152..8a903334431c 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 ce79f433460d..8b90f2e07576 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 b7a699a29e53..000000000000 --- 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 a37290f331bc..f40914109a10 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 8d1b420f3c17..74bb35272ec4 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 3c3bac70f4dc..4da0e2fda526 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 c2d2f36fde49..fb2f3b295a5b 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 26e0d6192b06..6d4832522fcb 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 c113ebb9a068..5347f947f5c9 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 54160eebf432..24503c9be3e4 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 1eafcb053246..ee2b140f143e 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/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 97d20789b7f6..de4defd27e70 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 54ec8976848c..2d01b5970dd1 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 593559b3bede..8853b6b7d9bc 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 8d30cd0db1ec..51a4ae805742 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 398c79f05681..000000000000 --- 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 9f1b59b1bfc8..354ad01f14a6 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 a699dd58b649..bac23305fb74 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 2cf8c9f6a3fa..6b0e3961a8fc 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 5bbf96e6cbc8..000000000000 --- 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 8493f5d9bcec..da708ab29bba 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 d70a6d74393f..000000000000 --- 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 16b03f133790..000000000000 --- 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 9e6d4ceaf16e..714890fd5f45 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 d6e896144cec..000000000000 --- 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 6111f198624c..000000000000 --- 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 95aec47dbf1f..547f66cad75c 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 72dd49504fe8..000000000000 --- 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 e35f292778ab..fe9fc76e06ba 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 a5199fe06249..1a6f7037c2ea 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 2fe5bc3f4a59..d51354a4fb7e 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 87b9f7c93a65..dd41d6e56a01 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 c484694e7d26..a0c4063da5d3 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 bbc7985f4519..41f8178bd9fa 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 a47d9b6b19cc..67552e6b76bc 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 3c353dd0d37e..bf21dc0876a8 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 c4a3c3776f50..1018f7b932da 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 71bd12ccc26c..315e739f4c58 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 3bb634717ca5..8a5faf302042 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 000000000000..919d64f316dd --- /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 0c58762280e3..cbd83d958f68 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/composer.json b/app/code/Magento/Elasticsearch7/composer.json index 05607765a089..89f41bf14b0d 100644 --- a/app/code/Magento/Elasticsearch7/composer.json +++ b/app/code/Magento/Elasticsearch7/composer.json @@ -5,7 +5,7 @@ "php": "~8.1.0||~8.2.0", "magento/framework": "*", "magento/module-elasticsearch": "*", - "elasticsearch/elasticsearch": "~7.17.0", + "elasticsearch/elasticsearch": "^7.17", "magento/module-advanced-search": "*", "magento/module-catalog-search": "*", "magento/module-search": "*" diff --git a/app/code/Magento/Elasticsearch7/etc/adminhtml/system.xml b/app/code/Magento/Elasticsearch7/etc/adminhtml/system.xml index 6b5d3cf36886..a8f4ecccdea9 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 c2df17556949..689d6ae80069 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 40320b9ffc84..c4f6784aaa79 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 d2c6a4e6901c..3844f0a1e3db 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 000000000000..ea871e884e6a --- /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 40f7b48b2112..2487c288af11 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 89b07e4be44e..a2d22a14acd2 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 a47660810cb4..4f025ae10627 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 2cdb79552e0f..fc293b41e0f7 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 6f486c15472c..88f7b81ea2ea 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/Email/view/frontend/web/logo_email.png b/app/code/Magento/Email/view/frontend/web/logo_email.png index 215e9d06edcd..ac822941c785 100644 Binary files a/app/code/Magento/Email/view/frontend/web/logo_email.png and b/app/code/Magento/Email/view/frontend/web/logo_email.png differ diff --git a/app/code/Magento/EncryptionKey/Model/ResourceModel/Key/Change.php b/app/code/Magento/EncryptionKey/Model/ResourceModel/Key/Change.php index e687817be743..5bb2b28c3e3a 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 ee28c66b80c4..1d4f642ac603 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 02e94d041010..46b0af6a41ba 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 10787d056a18..3aa71154de41 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 892738b450f2..705e3a66ddee 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 d1f29cf2f8b5..3e847bd4e35a 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 000000000000..33cd6f64de9c --- /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 9b36c722c9ef..000000000000 --- 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 000000000000..482534483c68 --- /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 641ed68a4660..419d9771987f 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 0f75d475d6b1..b7983a8be682 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 c2678153d13f..0e34ae3110a3 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 238b5175ae24..6448475affbb 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 000000000000..56626222312b --- /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 124d80b7cf4e..1d85cd923dd2 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 f164a8e21e0a..93e42704673b 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 1d9defb62efe..1e5522fd726a 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 c542b1f04d1e..41f12bddc247 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 3629bb424f20..000000000000 --- 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 2f3feecb5808..000000000000 --- 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 439d032a61fd..000000000000 --- 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 a449bf41dbd6..000000000000 --- 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 77f53e02a539..000000000000 --- 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 d1509d42730b..fb7ec12d2e4a 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 445ba54ac4d9..0f9b925746f1 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 127b61e3c2c5..ba3bb3962b06 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 9da9c8cac148..712f66893017 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/GiftMessageGraphQl/README.md b/app/code/Magento/GiftMessageGraphQl/README.md index 1b38bbc5ff57..485b403bbc34 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 eb28c1af96b9..d79a7837149d 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 050f8711027e..68f49ff1ebee 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 0370174c0b7f..3be62a588efa 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 bfc5bcc6eb39..226871406e24 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 a367a938d45b..8088b03707b2 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 ab5824a276a5..1597db4f80ec 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 612297081a26..d5985c308bbc 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 72915f4464c8..617ed65693f0 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 7c0330740a15..3aea6acb915b 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 83202eacdcd8..2d2a32562f82 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 000000000000..643b854a7ad0 --- /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 f03079c89bc6..f20956407c25 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 d42676f5dd1b..56351c7711ce 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 91e2518bfc63..8f9b4a83c0dd 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 000000000000..b6598e561100 --- /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 000000000000..c9f1c943a71e --- /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 000000000000..f3fb9d998897 --- /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 000000000000..5ce30093d46e --- /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 000000000000..8bb4c11a056d --- /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 000000000000..aeec59e21f39 --- /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 d8fa03657e40..5eb03d4ed13d 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 646459465fc5..6b96150157ad 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 55f25c176ed4..95d28ca46542 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 9403ccaf0709..bae3ceabf278 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 ff330ce38375..1372e5760e93 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 000000000000..e3009d73723f --- /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 d181c93a7010..af1fe042c6df 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.5" + "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 76bfb2118dc3..85a2636fdaba 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 1ba190cd8bb2..0688965af4cd 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 a594dcd6148f..c4ce6cb4e7ec 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 451e1039eec5..da29ccb83bb7 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 53f5155f8a3a..d43fd26dc5f5 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 10fe5739c461..e5a703de3399 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 ab0581127ace..85e03391eb9e 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 000000000000..6f56143def82 --- /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 000000000000..dd45b3c715f9 --- /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 1270ba24c94b..1a85f02b5be9 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 000000000000..c9bef893a25b --- /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 000000000000..ccf3bb972720 --- /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 000000000000..659967a2a7ff --- /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 000000000000..957cbea16a29 --- /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 000000000000..aedd667b01f7 --- /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 000000000000..bbf952823cbd --- /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 000000000000..dd334496bd50 --- /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 000000000000..7e2d03f78bb6 --- /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 000000000000..1df48e5d3086 --- /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 000000000000..808a2705d881 --- /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 000000000000..2616bdb0e213 --- /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 000000000000..ce03cb0ff469 --- /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 000000000000..f88e3ecb9b91 --- /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 000000000000..a8e4cee7f870 --- /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 000000000000..98698f49bc04 --- /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 000000000000..9d1e1d6db753 --- /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 000000000000..120b4d45f951 --- /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 000000000000..4840643d2bf7 --- /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 000000000000..bdc9a6788f63 --- /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 000000000000..950ec9baeb41 --- /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 000000000000..7e53dd1c70b4 --- /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 000000000000..82d45f3ccb32 --- /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 000000000000..179629864915 --- /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 000000000000..86945db807dc --- /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 000000000000..c4cd0165f15d --- /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 000000000000..bd860cd5cde1 --- /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 000000000000..f2ce961f312d --- /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 000000000000..ab7abfc5ff71 --- /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 000000000000..e101723035b8 --- /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 000000000000..d73b69c86e70 --- /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 000000000000..92667d350167 --- /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 000000000000..5633cd8b713d --- /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 000000000000..060abbe09cf6 --- /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 000000000000..6639cd1c7f90 --- /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 000000000000..db3844a297f2 --- /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 000000000000..e091fa98baaf --- /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 5091aedd14f5..3930fcffa6e0 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 4907126b941f..9cb5409163a7 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'], @@ -143,11 +153,38 @@ public function saveData() } } } - $this->links->saveLinksData($linksData); + $this->links->saveLinksData($linksData, $this->_entityModel); } 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/Model/Import/Product/Type/Grouped/Links.php b/app/code/Magento/GroupedImportExport/Model/Import/Product/Type/Grouped/Links.php index 104e69f2f92e..23c9d519987c 100644 --- a/app/code/Magento/GroupedImportExport/Model/Import/Product/Type/Grouped/Links.php +++ b/app/code/Magento/GroupedImportExport/Model/Import/Product/Type/Grouped/Links.php @@ -5,6 +5,7 @@ */ namespace Magento\GroupedImportExport\Model\Import\Product\Type\Grouped; +use Magento\CatalogImportExport\Model\Import\Product as ProductImport; use Magento\Framework\App\ResourceConnection; /** @@ -24,6 +25,8 @@ class Links /** * @var \Magento\ImportExport\Model\ImportFactory + * @deprecated + * @see no longer used */ protected $importFactory; @@ -55,16 +58,19 @@ public function __construct( } /** + * Saves the linksData to database + * * @param array $linksData + * @param ProductImport $productImport * @return void */ - public function saveLinksData($linksData) + public function saveLinksData(array $linksData, ProductImport $productImport) { $mainTable = $this->productLink->getMainTable(); $relationTable = $this->productLink->getTable('catalog_product_relation'); // save links and relations if ($linksData['product_ids']) { - $this->deleteOldLinks(array_keys($linksData['product_ids'])); + $this->deleteOldLinks(array_keys($linksData['product_ids']), $productImport); $mainData = []; foreach ($linksData['relation'] as $productData) { $mainData[] = [ @@ -76,7 +82,6 @@ public function saveLinksData($linksData) $this->connection->insertOnDuplicate($mainTable, $mainData); $this->connection->insertOnDuplicate($relationTable, $linksData['relation']); } - $attributes = $this->getAttributes(); // save positions and default quantity if ($linksData['attr_product_ids']) { @@ -107,13 +112,16 @@ public function saveLinksData($linksData) } /** + * Deletes all the product links in database that are linked to productIds + * * @param array $productIds + * @param ProductImport $productImport * @throws \Magento\Framework\Exception\LocalizedException * @return void */ - protected function deleteOldLinks($productIds) + protected function deleteOldLinks($productIds, ProductImport $productImport) { - if ($this->getBehavior() != \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) { + if ($this->getBehavior($productImport) != \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) { $this->connection->delete( $this->productLink->getMainTable(), $this->connection->quoteInto( @@ -125,6 +133,8 @@ protected function deleteOldLinks($productIds) } /** + * Gets all the attributes from database for the Grouped Link Type + * * @return array */ public function getAttributes() @@ -145,6 +155,8 @@ public function getAttributes() } /** + * Returns the integer id for Link Type + * * @return int */ protected function getLinkTypeId() @@ -155,12 +167,15 @@ protected function getLinkTypeId() /** * Retrieve model behavior * + * @param ProductImport $productImport * @return string */ - protected function getBehavior() + protected function getBehavior(ProductImport $productImport) { if ($this->behavior === null) { - $this->behavior = $this->importFactory->create()->getDataSourceModel()->getBehavior(); + $ids = $productImport->getIds(); + $dataSourceModel = $productImport->getDataSourceModel(); + $this->behavior = $dataSourceModel->getBehavior($ids); } return $this->behavior; } diff --git a/app/code/Magento/GroupedImportExport/README.md b/app/code/Magento/GroupedImportExport/README.md index 28b66412d97c..fd055be68bdb 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 fd30f4bf488b..818419a42ad0 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/Grouped/LinksTest.php b/app/code/Magento/GroupedImportExport/Test/Unit/Model/Import/Product/Type/Grouped/LinksTest.php index 983e5b77e3e9..4aa2d85aa53a 100644 --- a/app/code/Magento/GroupedImportExport/Test/Unit/Model/Import/Product/Type/Grouped/LinksTest.php +++ b/app/code/Magento/GroupedImportExport/Test/Unit/Model/Import/Product/Type/Grouped/LinksTest.php @@ -9,12 +9,12 @@ namespace Magento\GroupedImportExport\Test\Unit\Model\Import\Product\Type\Grouped; use Magento\Catalog\Model\ResourceModel\Product\Link; +use Magento\CatalogImportExport\Model\Import\Product as ProductImport; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\Pdo\Mysql; use Magento\Framework\DB\Select; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\GroupedImportExport\Model\Import\Product\Type\Grouped\Links; -use Magento\ImportExport\Model\Import; use Magento\ImportExport\Model\ImportFactory; use Magento\ImportExport\Model\ResourceModel\Import\Data; use PHPUnit\Framework\MockObject\MockObject; @@ -28,20 +28,20 @@ class LinksTest extends TestCase /** @var ObjectManagerHelper */ protected $objectManagerHelper; - /** @var Link|MockObject */ + /** @var Link&MockObject */ protected $link; - /** @var ResourceConnection|MockObject */ + /** @var ResourceConnection&MockObject */ protected $resource; /** @var Mysql */ protected $connection; - /** @var ImportFactory|MockObject */ + /** @var ImportFactory&MockObject */ protected $importFactory; - /** @var Import|MockObject */ - protected $import; + /** @var ProductImport&MockObject */ + protected $productImport; protected function setUp(): void { @@ -52,11 +52,7 @@ protected function setUp(): void ->expects($this->once()) ->method('getConnection') ->willReturn($this->connection); - - $this->import = $this->createMock(Import::class); $this->importFactory = $this->createPartialMock(ImportFactory::class, ['create']); - $this->importFactory->expects($this->any())->method('create')->willReturn($this->import); - $this->objectManagerHelper = new ObjectManagerHelper($this); $this->links = $this->objectManagerHelper->getObject( Links::class, @@ -66,6 +62,8 @@ protected function setUp(): void 'importFactory' => $this->importFactory ] ); + $this->productImport = $this->createMock(ProductImport::class); + $this->productImport->expects($this->any())->method('getIds')->willReturn([]); } /** @@ -95,7 +93,7 @@ public function testSaveLinksDataNoProductsAttrs($linksData) $attributes = $this->attributesDataProvider(); $this->processAttributeGetter($attributes[2]['dbAttributes']); $this->connection->expects($this->exactly(2))->method('insertOnDuplicate'); - $this->links->saveLinksData($linksData); + $this->links->saveLinksData($linksData, $this->productImport); } /** @@ -125,8 +123,7 @@ public function testSaveLinksDataWithProductsAttrs($linksData) $this->link->expects($this->exactly(2))->method('getAttributeTypeTable')->willReturn( 'table_name' ); - - $this->links->saveLinksData($linksData); + $this->links->saveLinksData($linksData, $this->productImport); } /** @@ -197,6 +194,6 @@ protected function processBehaviorGetter($behavior) { $dataSource = $this->createMock(Data::class); $dataSource->expects($this->once())->method('getBehavior')->willReturn($behavior); - $this->import->expects($this->once())->method('getDataSourceModel')->willReturn($dataSource); + $this->productImport->expects($this->any())->method('getDataSourceModel')->willReturn($dataSource); } } 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 80fff5f2b12b..dfb57462cd58 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 b2b3fffce018..986b8f20791e 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 4b79f8f10983..480ae502cc8f 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 9ff50bfd2ce8..cacacdf3353c 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 990f8405c524..5d55b3c4bbe1 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 d5dcd7f48b95..456982014c60 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 55144884c719..610554d0d0f8 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 68f9da93ec99..2980a6cefa54 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 ef1665d96520..16c4c29c18a6 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 8d808cd07a87..8d72e5f4bca3 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 0dc622a82aaa..9d33ca36bd48 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 b51b14f25409..cbef10f3da28 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 f39e18373893..ab9949079331 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/Unit/Model/ProductTest.php b/app/code/Magento/GroupedProduct/Test/Unit/Model/ProductTest.php index f31b3a3db9d8..94331fc65278 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 000000000000..f8881837c6d4 --- /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 0257d87a2d9e..996c61571563 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 000000000000..ed29bd9d4b14 --- /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 b2336a074129..3bc9036e6f9b 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 f3aa6be9ed4f..f29f3098ae03 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 86940a401f10..84eab3bf132a 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 6af830556edb..1df309fe105e 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/Block/Adminhtml/Import/Edit/Form.php b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php index ac966667fe23..f4ef5ec432ba 100644 --- a/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php +++ b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php @@ -209,7 +209,6 @@ protected function _prepareForm() ); $fieldsets[$behaviorCode] = $fieldset; } - // fieldset for file uploading $fieldset = $form->addFieldset( 'upload_file_fieldset', @@ -255,11 +254,19 @@ protected function _prepareForm() ), ] ); + $fieldset->addField( + Import::FIELD_IMPORT_IDS, + 'hidden', + [ + 'name' => Import::FIELD_IMPORT_IDS, + 'label' => __('Import id'), + 'title' => __('Import id'), + 'value' => '', + ] + ); $fieldsets['upload'] = $fieldset; - $form->setUseContainer(true); $this->setForm($form); - return parent::_prepareForm(); } diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php b/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php index 9dcb2fdafb74..6fd229f26a1a 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 ebf88e6c68e2..b74f48685fee 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/Import/Validate.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php index 4be73fe384ae..c388851edcbe 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php @@ -42,15 +42,15 @@ public function execute() //phpcs:disable Magento2.Security.Superglobal if ($data) { // common actions - $resultBlock->addAction( - 'show', - 'import_validation_container' - ); - + $resultBlock->addAction('show', 'import_validation_container'); $import = $this->getImport()->setData($data); try { $source = $import->uploadFileAndGetSource(); $this->processValidationResult($import->validateSource($source), $resultBlock); + $ids = $import->getValidatedIds(); + if (count($ids) > 0) { + $resultBlock->addAction('value', Import::FIELD_IMPORT_IDS, $ids); + } } catch (\Magento\Framework\Exception\LocalizedException $e) { $resultBlock->addError($e->getMessage()); } catch (\Exception $e) { @@ -117,7 +117,6 @@ private function processValidationResult($validationResult, $resultBlock) * Provides import model. * * @return Import - * @deprecated 100.1.0 */ private function getImport() { diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php b/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php index 4092879e2362..81347ce41a90 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 033f9849b738..1252a0665009 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 e83f508037da..7623677a4781 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 d9dd98bc54cd..3e0b403089ac 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 f5a993ae01ce..b2ae3867a8b5 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 95e570ff2d88..ee8059a9780d 100644 --- a/app/code/Magento/ImportExport/Model/Import.php +++ b/app/code/Magento/ImportExport/Model/Import.php @@ -28,9 +28,9 @@ use Magento\ImportExport\Model\Import\ConfigInterface; use Magento\ImportExport\Model\Import\Entity\AbstractEntity; use Magento\ImportExport\Model\Import\Entity\Factory; +use Magento\ImportExport\Model\Import\EntityInterface; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; -use Magento\ImportExport\Model\Import\Source\Base64EncodedCsvData; use Magento\ImportExport\Model\ResourceModel\Import\Data; use Magento\ImportExport\Model\Source\Import\AbstractBehavior; use Magento\ImportExport\Model\Source\Import\Behavior\Factory as BehaviorFactory; @@ -98,6 +98,11 @@ class Import extends AbstractModel */ public const FIELD_EMPTY_ATTRIBUTE_VALUE_CONSTANT = '_import_empty_attribute_value_constant'; + /** + * Id of the `importexport_importdata` row after validation. + */ + public const FIELD_IMPORT_IDS = '_import_ids'; + /** * Allow multiple values wrapping in double quotes for additional attributes. */ @@ -118,7 +123,7 @@ class Import extends AbstractModel public const IMPORT_DIR = 'import/'; /** - * @var AbstractEntity|ImportAbstractEntity + * @var EntityInterface */ protected $_entityAdapter; @@ -205,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 @@ -224,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( @@ -244,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; @@ -265,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 AbstractEntity|ImportAbstractEntity + * @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(); @@ -475,7 +507,22 @@ public function getWorkingDir() */ public function importSource() { - $ids = $this->_getEntityAdapter()->getIds(); + 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)); @@ -507,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); @@ -527,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. * @@ -617,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 * @@ -627,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')); @@ -653,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; } /** @@ -852,4 +943,14 @@ public function getDeletedItemsCount() { return $this->_getEntityAdapter()->getDeletedItemsCount(); } + + /** + * Retrieve Ids of Validated Rows + * + * @return int[] + */ + public function getValidatedIds() : array + { + return $this->_getEntityAdapter()->getIds() ?? []; + } } diff --git a/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php b/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php index 1470470c4b91..d9b990865937 100644 --- a/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php +++ b/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php @@ -14,9 +14,9 @@ use Magento\ImportExport\Model\Import; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; -use Magento\ImportExport\Model\Import\Source\Base64EncodedCsvData; use Magento\ImportExport\Model\ImportFactory; use Magento\ImportExport\Model\ResourceModel\Helper; +use Magento\ImportExport\Model\ResourceModel\Import\Data as DataSourceModel; use Magento\Store\Model\ScopeInterface; /** @@ -26,9 +26,10 @@ * @api * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @since 100.0.2 */ -abstract class AbstractEntity +abstract class AbstractEntity implements EntityInterface { /** * Custom row import behavior column name @@ -121,7 +122,7 @@ abstract class AbstractEntity /** * DB data source model * - * @var \Magento\ImportExport\Model\ResourceModel\Import\Data + * @var DataSourceModel */ protected $_dataSourceModel; @@ -421,9 +422,7 @@ protected function _saveValidatedBunches() $startNewBunch = false; $source->rewind(); - if (!$source instanceof Base64EncodedCsvData) { - $this->_dataSourceModel->cleanBunches(); - } + $this->_dataSourceModel->cleanProcessedBunches(); $mainAttributeCode = $this->getMasterAttributeCode(); while ($source->valid() || count($bunchRows) || isset($entityGroup)) { @@ -442,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) { @@ -497,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. * @@ -695,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': @@ -893,9 +931,9 @@ public function getDeletedItemsCount() */ protected function updateItemsCounterStats(array $created = [], array $updated = [], array $deleted = []) { - $this->countItemsCreated = count($created); - $this->countItemsUpdated = count($updated); - $this->countItemsDeleted = count($deleted); + $this->countItemsCreated += count($created); + $this->countItemsUpdated += count($updated); + $this->countItemsDeleted += count($deleted); return $this; } @@ -914,8 +952,29 @@ public function getValidColumnNames() * * @return array */ - public function getIds() + public function getIds() : array { return $this->ids; } + + /** + * Set Ids of Validated Rows + * + * @param array $ids + * @return void + */ + public function setIds(array $ids) + { + $this->ids = $ids; + } + + /** + * Gets the currently used DataSourceModel + * + * @return DataSourceModel + */ + public function getDataSourceModel() : DataSourceModel + { + return $this->_dataSourceModel; + } } diff --git a/app/code/Magento/ImportExport/Model/Import/Adapter.php b/app/code/Magento/ImportExport/Model/Import/Adapter.php index 3c0d588dc3c5..6a438b1fc1e8 100644 --- a/app/code/Magento/ImportExport/Model/Import/Adapter.php +++ b/app/code/Magento/ImportExport/Model/Import/Adapter.php @@ -6,12 +6,9 @@ namespace Magento\ImportExport\Model\Import; use Magento\Framework\Filesystem\Directory\Write; -use Magento\ImportExport\Model\Import\Source\Factory; /** * Import adapter model - * @Deprecated - * @see \Magento\ImportExport\Model\Import\Source\Factory */ class Adapter { diff --git a/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php b/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php index 21a1e2a4e24c..eb2d56018b31 100644 --- a/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php +++ b/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php @@ -9,11 +9,12 @@ use Magento\Framework\App\ResourceConnection; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Serialize\Serializer\Json; -use Magento\ImportExport\Model\Import\AbstractSource; use Magento\ImportExport\Model\Import as ImportExport; +use Magento\ImportExport\Model\Import\AbstractSource; +use Magento\ImportExport\Model\Import\EntityInterface; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; -use Magento\ImportExport\Model\Import\Source\Base64EncodedCsvData; +use Magento\ImportExport\Model\ResourceModel\Import\Data as DataSourceModel; /** * Import entity abstract model @@ -25,7 +26,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ -abstract class AbstractEntity +abstract class AbstractEntity implements EntityInterface { /** * Database constants @@ -102,7 +103,7 @@ abstract class AbstractEntity /** * DB data source model. * - * @var \Magento\ImportExport\Model\ResourceModel\Import\Data + * @var DataSourceModel */ protected $_dataSourceModel; @@ -395,10 +396,7 @@ protected function _saveValidatedBunches() $skuSet = []; $source->rewind(); - if (!$source instanceof Base64EncodedCsvData) { - $this->_dataSourceModel->cleanBunches(); - } - + $this->_dataSourceModel->cleanProcessedBunches(); while ($source->valid() || $bunchRows) { if ($startNewBunch || !$source->valid()) { $this->ids[] = @@ -905,7 +903,7 @@ protected function getMetadataPool() * * @return array */ - public function getIds() + public function getIds() : array { return $this->ids; } @@ -920,4 +918,14 @@ public function setIds(array $ids) { $this->ids = $ids; } + + /** + * Gets the currently used DataSourceModel + * + * @return DataSourceModel + */ + public function getDataSourceModel() : DataSourceModel + { + return $this->_dataSourceModel; + } } diff --git a/app/code/Magento/ImportExport/Model/Import/EntityInterface.php b/app/code/Magento/ImportExport/Model/Import/EntityInterface.php new file mode 100644 index 000000000000..12ed77382497 --- /dev/null +++ b/app/code/Magento/ImportExport/Model/Import/EntityInterface.php @@ -0,0 +1,209 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Model\Import; + +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; +use Magento\ImportExport\Model\ResourceModel\Import\Data as DataSourceModel; + +/** + * Import entity interface + * + * @api + */ +interface EntityInterface +{ + + /** + * Returns Error aggregator + * + * @return ProcessingErrorAggregatorInterface + */ + public function getErrorAggregator(); + + /** + * Imported entity type code getter + * + * @abstract + * @return string + */ + public function getEntityTypeCode(); + + /** + * Add error with corresponding current data source row number. + * + * @param string $errorCode Error code or simply column name + * @param int $errorRowNum Row number. + * @param string $colName OPTIONAL Column name. + * @param string $errorMessage OPTIONAL Column name. + * @param string $errorLevel + * @param string $errorDescription + * @return $this + */ + public function addRowError( + $errorCode, + $errorRowNum, + $colName = null, + $errorMessage = null, + $errorLevel = ProcessingError::ERROR_LEVEL_CRITICAL, + $errorDescription = null + ); + + /** + * Add message template for specific error code from outside + * + * @param string $errorCode Error code + * @param string $message Message template + * @return $this + */ + public function addMessageTemplate($errorCode, $message); + + /** + * Returns number of checked entities + * + * @return int + */ + public function getProcessedEntitiesCount(); + + /** + * Returns number of checked rows + * + * @return int + */ + public function getProcessedRowsCount(); + + /** + * Source object getter + * + * @return AbstractSource + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function getSource(); + + /** + * Import process start + * + * @return bool Result of operation + */ + public function importData(); + + /** + * Is attribute contains particular data (not plain entity attribute) + * + * @param string $attributeCode + * @return bool + */ + public function isAttributeParticular($attributeCode); + + /** + * Import possibility getter + * + * @return bool + */ + public function isImportAllowed(); + + /** + * Returns TRUE if row is valid and not in skipped rows array + * + * @param array $rowData + * @param int $rowNumber + * @return bool + */ + public function isRowAllowedToImport(array $rowData, $rowNumber); + + /** + * Is import need to log in history. + * + * @return bool + */ + public function isNeedToLogInHistory(); + + /** + * Validate data row + * + * @param array $rowData + * @param int $rowNumber + * @return bool + */ + public function validateRow(array $rowData, $rowNumber); + + /** + * Set data from outside to change behavior + * + * @param array $parameters + * @return $this + */ + public function setParameters(array $parameters); + + /** + * Source model setter + * + * @param AbstractSource $source + * @return $this + */ + public function setSource(AbstractSource $source); + + /** + * Validate data + * + * @return ProcessingErrorAggregatorInterface + * @throws \Magento\Framework\Exception\LocalizedException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function validateData(); + + /** + * Get count of created items + * + * @return int + */ + public function getCreatedItemsCount(); + + /** + * Get count of updated items + * + * @return int + */ + public function getUpdatedItemsCount(); + + /** + * Get count of deleted items + * + * @return int + */ + public function getDeletedItemsCount(); + + /** + * Retrieve valid column names + * + * @return array + */ + public function getValidColumnNames(); + + /** + * Retrieve Ids of Validated Rows + * + * @return array + */ + public function getIds() : array; + + /** + * Set Ids of Validated Rows + * + * @param array $ids + * @return void + */ + public function setIds(array $ids); + + /** + * Gets the currently used DataSourceModel + * + * @return array + */ + public function getDataSourceModel() : DataSourceModel; +} diff --git a/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php b/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php index 2f8bfdcf70a5..bcc70eb9d326 100644 --- a/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php +++ b/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php @@ -26,6 +26,11 @@ class ProcessingErrorAggregator implements ProcessingErrorAggregatorInterface */ protected $items = []; + /** + * @var ProcessingError[] + */ + private $itemsByRowColumnAndCode = []; + /** * @var int[] */ @@ -87,13 +92,13 @@ public function addError( $this->processInvalidRow($rowNumber); } $errorMessage = $this->getErrorMessage($errorCode, $errorMessage, $columnName); - /** @var ProcessingError $newError */ $newError = $this->errorFactory->create(); $newError->init($errorCode, $errorLevel, $rowNumber, $columnName, $errorMessage, $errorDescription); $this->items['rows'][$rowNumber][] = $newError; $this->items['codes'][$errorCode][] = $newError; $this->items['messages'][$errorMessage][] = $newError; + $this->itemsByRowColumnAndCode[$rowNumber][$columnName][$errorCode] = $newError; return $this; } @@ -356,7 +361,7 @@ public function clear() $this->errorStatistics = []; $this->invalidRows = []; $this->skippedRows = []; - + $this->itemsByRowColumnAndCode = []; return $this; } @@ -370,13 +375,7 @@ public function clear() */ protected function isErrorAlreadyAdded($rowNum, $errorCode, $columnName = null) { - $errors = $this->getErrorsByCode([$errorCode]); - foreach ($errors as $error) { - if ($rowNum == $error->getRowNumber() && $columnName == $error->getColumnName()) { - return true; - } - } - return false; + return isset($this->itemsByRowColumnAndCode[$rowNum][$columnName][$errorCode]); } /** 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 000000000000..cb163edb55ce --- /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/Base64EncodedCsvData.php b/app/code/Magento/ImportExport/Model/Import/Source/Base64EncodedCsvData.php deleted file mode 100644 index 0215de4580ba..000000000000 --- a/app/code/Magento/ImportExport/Model/Import/Source/Base64EncodedCsvData.php +++ /dev/null @@ -1,62 +0,0 @@ -<?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; - -class Base64EncodedCsvData extends AbstractSource -{ - /** - * @var array - */ - private $rows; - - /** - * @var string - */ - private $delimiter = ','; - - /** - * Read Data and detect column names - * - * @param string $file - */ - public function __construct(string $file) - { - // phpcs:ignore Magento2.Functions.DiscouragedFunction - $source = trim(base64_decode($file)); - $rowsData = preg_split("/\r\n|\n|\r/", $source); - $colNames = explode($this->delimiter, $rowsData[0]); - $this->rows = array_splice($rowsData, 1); - parent::__construct($colNames); - } - - /** - * Read next line from CSV data - * - * @return array - */ - public function _getNextRow() - { - if ($this->_key===count($this->rows)) { - return []; - } - $parsed =str_getcsv($this->rows[$this->_key], ',', '"'); - if (is_array($parsed) && count($parsed) != $this->_colQty) { - foreach ($parsed as $element) { - if ($element && strpos($element, "'") !== false) { - $this->_foundWrongQuoteFlag = true; - break; - } - } - } else { - $this->_foundWrongQuoteFlag = false; - } - return is_array($parsed) ? $parsed : []; - } -} diff --git a/app/code/Magento/ImportExport/Model/Import/Source/Csv.php b/app/code/Magento/ImportExport/Model/Import/Source/Csv.php index e04ef9f9537b..178ca38ede0a 100644 --- a/app/code/Magento/ImportExport/Model/Import/Source/Csv.php +++ b/app/code/Magento/ImportExport/Model/Import/Source/Csv.php @@ -5,7 +5,8 @@ */ namespace Magento\ImportExport\Model\Import\Source; -use Magento\Framework\Filesystem\Directory\Read; +use Magento\Framework\Filesystem\Directory\Read as DirectoryRead; +use Magento\Framework\Filesystem\File\ReadInterface as FileReadInterface; /** * CSV import adapter @@ -13,7 +14,7 @@ class Csv extends \Magento\ImportExport\Model\Import\AbstractSource { /** - * @var \Magento\Framework\Filesystem\File\Write + * @var FileReadInterface */ protected $_file; @@ -42,27 +43,31 @@ class Csv extends \Magento\ImportExport\Model\Import\AbstractSource * * There must be column names in the first line * - * @param string $file - * @param Read $directory + * @param string|FileReadInterface $file + * @param DirectoryRead $directory * @param string $delimiter * @param string $enclosure * @throws \LogicException */ public function __construct( $file, - Read $directory, + DirectoryRead $directory, $delimiter = ',', $enclosure = '"' ) { - // phpcs:ignore Magento2.Functions.DiscouragedFunction - register_shutdown_function([$this, 'destruct']); - try { - $this->filePath = $directory->getRelativePath($file); - $this->_file = $directory->openFile($this->filePath, 'r'); + if ($file instanceof FileReadInterface) { + $this->filePath = ''; + $this->_file = $file; $this->_file->seek(0); - self::$openFiles[$this->filePath] = true; - } catch (\Magento\Framework\Exception\FileSystemException $e) { - throw new \LogicException("Unable to open file: '{$file}'"); + } else { + try { + $this->filePath = $directory->getRelativePath($file); + $this->_file = $directory->openFile($this->filePath, 'r'); + $this->_file->seek(0); + self::$openFiles[$this->filePath] = true; + } catch (\Magento\Framework\Exception\FileSystemException $e) { + throw new \LogicException("Unable to open file: '{$file}'"); + } } if ($delimiter) { $this->_delimiter = $delimiter; @@ -76,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/Factory.php b/app/code/Magento/ImportExport/Model/Import/Source/Factory.php deleted file mode 100644 index 3ea7534cafd9..000000000000 --- a/app/code/Magento/ImportExport/Model/Import/Source/Factory.php +++ /dev/null @@ -1,71 +0,0 @@ -<?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\Framework\Filesystem\Directory\Write; -use Magento\Framework\ObjectManagerInterface; -use Magento\ImportExport\Model\Import\AbstractSource; - -class Factory -{ - /** - * Object Manager Instance - * - * @var \Magento\Framework\ObjectManagerInterface - */ - private $objectManager; - - /** - * @param ObjectManagerInterface $objectManager - */ - public function __construct( - ObjectManagerInterface $objectManager - ) { - $this->objectManager = $objectManager; - } - - /** - * Create class instance with specified parameters - * - * @param string $source - * @param Write $directory - * @param mixed $options - * @return AbstractSource - * @phpcs:disable Magento2.Functions.DiscouragedFunction - * @throws \Magento\Framework\Exception\LocalizedException - */ - public function create($source, $directory = null, $options = null): AbstractSource - { - $adapterClass = 'Magento\ImportExport\Model\Import\Source\\'; - if (file_exists($source)) { - $type = ucfirst(strtolower(pathinfo($source, PATHINFO_EXTENSION))); - } else { - $type = 'Base64EncodedCsvData'; - } - if (!is_string($source) || !$source) { - throw new \Magento\Framework\Exception\LocalizedException( - __('The source type must be a non-empty string.') - ); - } - $adapterClass.= $type; - if (!class_exists($adapterClass)) { - throw new \Magento\Framework\Exception\LocalizedException( - __('\'%1\' file extension is not supported', $type) - ); - } - return $this->objectManager->create( - $adapterClass, - [ - 'file' => $source, - 'directory' => $directory, - 'options' => $options - ] - ); - } -} 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 000000000000..fbfdde9f4763 --- /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 000000000000..48e781c505d0 --- /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 000000000000..ab0743230e6e --- /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/ResourceModel/Import/Data.php b/app/code/Magento/ImportExport/Model/ResourceModel/Import/Data.php index 6240ebc8450a..81cd041c2731 100644 --- a/app/code/Magento/ImportExport/Model/ResourceModel/Import/Data.php +++ b/app/code/Magento/ImportExport/Model/ResourceModel/Import/Data.php @@ -125,9 +125,7 @@ public function cleanProcessedBunches() { $this->getConnection()->delete( $this->getMainTable(), - [ - 'is_processed' => '1' - ] + 'is_processed = 1 OR TIMESTAMPADD(DAY, 1, updated_at) < CURRENT_TIMESTAMP() ' ); } diff --git a/app/code/Magento/ImportExport/Model/Source/Upload.php b/app/code/Magento/ImportExport/Model/Source/Upload.php index fdddaaf1a4ab..c50d079e895e 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 000000000000..677d080b1d5d --- /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 9a130aee1102..a7a395c291cb 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 000000000000..e3688ebd0d5d --- /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 17b065ec7d88..92a160ebe2a7 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 d2eee7c3c5f4..d2b10d3d541b 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 361722ec10b9..4c547e17f1c2 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 9f286d5148a0..1a8f993b5e3c 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 b9039fa59f5c..a06e9b61bea9 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 8d405d7813cc..b8d22dc77d8f 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 503037401b9f..e07693178531 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 5fe42e707403..a4a49118d001 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 95a6a453e1e0..f3a36e6fef70 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 0403649d7add..cf64852c7b50 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 3a4bd2507e8b..c7dce4b50a6b 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 2811852fefaf..ec03c03d052a 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 6c2d7f76cce3..9910c5f91886 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/Unit/Block/Adminhtml/Export/FilterTest.php b/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Export/FilterTest.php index cf69cdf7ee36..a7b1501e32ea 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 d153c169bfdd..d31d22a97a38 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 57e33a1dd51a..7c8e06d3f681 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 2f10ce42f84d..1a5677c555b1 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 a65b552182f5..d620b33fa39b 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 8a3621cc9ff1..3ee7e4b64638 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 abef693c9aca..815680df2908 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 03c6356fc1ad..40e9191c17bd 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 bd3bf6711ced..3675f012c503 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 ed9c74b92dbe..e35eca06f2fd 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 295b706e3e0b..129a4cd3a944 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 3f5b40cef798..5239df3e9b36 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 000000000000..5e9224713fc0 --- /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 000000000000..dd13dc6b4c97 --- /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 000000000000..a893d259fb91 --- /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 000000000000..2a4ec9919826 --- /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 8367de38d2f6..edbeb96f64f5 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 7b124957d5f5..cb09c448cf0c 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/db_schema.xml b/app/code/Magento/ImportExport/etc/db_schema.xml index adad0c0d3042..f31b3499f0dc 100644 --- a/app/code/Magento/ImportExport/etc/db_schema.xml +++ b/app/code/Magento/ImportExport/etc/db_schema.xml @@ -13,6 +13,8 @@ <column xsi:type="varchar" name="behavior" nullable="false" length="10" default="append" comment="Behavior"/> <column xsi:type="longtext" name="data" nullable="true" comment="Data"/> <column xsi:type="boolean" name="is_processed" nullable="false" default="true" comment="Is Row Processed"/> + <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" + comment="timestamp of last update"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> diff --git a/app/code/Magento/ImportExport/etc/db_schema_whitelist.json b/app/code/Magento/ImportExport/etc/db_schema_whitelist.json index 366b74d7359f..768b3ed6ef96 100644 --- a/app/code/Magento/ImportExport/etc/db_schema_whitelist.json +++ b/app/code/Magento/ImportExport/etc/db_schema_whitelist.json @@ -5,7 +5,8 @@ "entity": true, "behavior": true, "data": true, - "is_processed": true + "is_processed": true, + "updated_at": true }, "constraint": { "PRIMARY": true diff --git a/app/code/Magento/ImportExport/etc/di.xml b/app/code/Magento/ImportExport/etc/di.xml index b4c65aaf5ef1..66930b2127d5 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 a91a76612fd9..cc1098841bab 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 adfe3dd5b346..99e985f813c6 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/IndexerReindexCommand.php b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php index 285b06e95331..376dabe00ac5 100644 --- a/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php +++ b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php @@ -101,7 +101,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $output->write($indexer->getTitle() . ' index '); - $startTime = microtime(true); + $startTime = new \DateTimeImmutable(); $indexerConfig = $this->getConfig()->getIndexer($indexer->getId()); $sharedIndex = $indexerConfig['shared_index'] ?? null; @@ -112,10 +112,15 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->sharedIndexesComplete[] = $sharedIndex; } } - $resultTime = microtime(true) - $startTime; + $endTime = new \DateTimeImmutable(); + $interval = $startTime->diff($endTime); + $days = $interval->format('%d'); + $hours = $days > 0 ? $days * 24 + $interval->format('%H') : $interval->format('%H'); + $minutes = $interval->format('%I'); + $seconds = $interval->format('%S'); $output->writeln( - __('has been rebuilt successfully in %time', ['time' => gmdate('H:i:s', (int) $resultTime)]) + __('has been rebuilt successfully in %1:%2:%3', $hours, $minutes, $seconds) ); } catch (\Throwable $e) { $output->writeln('process error during indexation process:'); @@ -238,7 +243,9 @@ private function validateIndexerStatus(IndexerInterface $indexer) * Get config * * @return ConfigInterface - * @deprecated 100.1.0 + * @deprecated 100.1.0 We don't recommend this approach anymore + * @see Add a new optional parameter to the constructor at the end of the arguments list instead + * and fetch the dependency using Magento\Framework\App\ObjectManager::getInstance() in the constructor body */ private function getConfig() { @@ -252,7 +259,9 @@ private function getConfig() * Get dependency info provider * * @return DependencyInfoProvider - * @deprecated 100.2.0 + * @deprecated 100.2.0 We don't recommend this approach anymore + * @see Add a new optional parameter to the constructor at the end of the arguments list instead + * and fetch the dependency using Magento\Framework\App\ObjectManager::getInstance() in the constructor body */ private function getDependencyInfoProvider() { diff --git a/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand.php b/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand.php index 51d67e2116a0..0020a0592aa3 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 000000000000..67a2de8dacac --- /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 8909fa999528..eedd7797c01f 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 f8c3c58f5413..19b62817df8a 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 ac8b9590e58f..7be1d5a3a9e2 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 2f240095193a..b5cd331fbf85 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 086d06a88fa8..d7146f75577b 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 b6fd158364de..5e7382013de9 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 78b8fa070b15..7846421daa70 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 2cba0b43be0d..0285d3400924 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 000000000000..7b0ea1197c42 --- /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 e7e7ba82bf09..44f70263c5df 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 a8aa089a389e..6ed57a1be98b 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 7b77af08614c..b5fc423bbb74 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 a1bfae067a2a..b99777d594d8 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 a00b2516d308..36f6e6e07d09 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 825358e74f2a..c4593f269c18 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 d92e70cd1993..75d61fe2261f 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/Unit/Block/Backend/Grid/Column/Renderer/ScheduledTest.php b/app/code/Magento/Indexer/Test/Unit/Block/Backend/Grid/Column/Renderer/ScheduledTest.php index c48da82ed3d7..2d83d27a368e 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/Console/Command/IndexerReindexCommandTest.php b/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerReindexCommandTest.php index 244798e7261b..4db91dcfb7cb 100644 --- a/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerReindexCommandTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerReindexCommandTest.php @@ -8,13 +8,11 @@ namespace Magento\Indexer\Test\Unit\Console\Command; use Magento\Framework\Console\Cli; -use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Indexer\Config\DependencyInfoProvider; use Magento\Framework\Indexer\ConfigInterface; use Magento\Framework\Indexer\IndexerInterface; use Magento\Framework\Indexer\IndexerRegistry; use Magento\Framework\Indexer\StateInterface; -use Magento\Framework\Phrase; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Indexer\Console\Command\IndexerReindexCommand; use Magento\Indexer\Model\Config; @@ -27,7 +25,7 @@ */ class IndexerReindexCommandTest extends AbstractIndexerCommandCommonSetup { - const STUB_INDEXER_NAME = 'Indexer Name'; + private const STUB_INDEXER_NAME = 'Indexer Name'; /** * Command being tested * @@ -130,6 +128,11 @@ public function testExecuteAll() self::STUB_INDEXER_NAME . ' index has been rebuilt successfully in', $actualValue ); + $this->assertMatchesRegularExpression( + '/' . self::STUB_INDEXER_NAME + . ' index has been rebuilt successfully in (?:(?:([01]?\d|2[0-3]):)?([0-5]?\d):)?([0-5]?\d)/m', + $actualValue + ); } /** 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 649db0282d12..11345aa98863 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 bcdfbea78b0b..451d4c211ead 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 ca11c571a405..1de2b3fa04e7 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 b0a339551955..ba6216f37f7d 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 8cee48610c7e..54388ac7fdeb 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 482ca591811b..16526c13a41d 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 cd84bb9cf215..4942b5e077e6 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 000000000000..7c1ab32dbd97 --- /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 66b14b0c72c8..f92335e4c470 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 c81c6d36786e..f367d75b5012 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 4248c15b50e0..93fc043b5341 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/etc/di.xml b/app/code/Magento/InstantPurchase/etc/di.xml index def091d285da..40debf28e254 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 5f5e6b990d1d..c9caeb63a955 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 3ff9e061c749..3b9b1792d1cf 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 6d4e4ed39f6e..2e51b34b6b3b 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 d7dca53888f9..a735a49cabee 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 0148278ac7aa..dbb3d005f724 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 509521038d4f..ed1de47c64af 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 a1a9641f6be3..a29c8e5b56e6 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 49557be6657b..d8ad46887e27 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 c88571ca5ada..c44396910c14 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/Unit/Model/Config/Consolidated/XsdTest.php b/app/code/Magento/Integration/Test/Unit/Model/Config/Consolidated/XsdTest.php index 5333a312e018..3da82625662f 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 284e3bad1aa6..16eb0a9718f1 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 72ae3dd18e0a..5bac267acefb 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/Integration/etc/webapi.xml b/app/code/Magento/Integration/etc/webapi.xml index 8814fe5bb005..6c6dc1a9def9 100644 --- a/app/code/Magento/Integration/etc/webapi.xml +++ b/app/code/Magento/Integration/etc/webapi.xml @@ -19,4 +19,13 @@ <resource ref="anonymous"/> </resources> </route> + <route url="/V1/integration/customer/revoke-customer-token" method="POST"> + <service class="Magento\Integration\Api\CustomerTokenServiceInterface" method="revokeCustomerAccessToken"/> + <resources> + <resource ref="self"/> + </resources> + <data> + <parameter name="customerId" force="true">%customer_id%</parameter> + </data> + </route> </routes> diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JweAlgorithmManagerFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JweAlgorithmManagerFactory.php index a1a53db3e450..9668a0cd5170 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 ded1e63fabf2..c15650701adc 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 e9478727b559..4ef45440ebbb 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 811dc1948c12..d3bb5db7439f 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 77f96ef0c564..0d324c2a6c2f 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 5f74a0c04467..a66662080e22 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 c01f80b7bcb9..dcb0089f0b29 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 80280178e459..19bbf499906c 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 e113a4cda82e..8d1f2032004f 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 e7da263a6477..9cc3fbb01d25 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 6e256d3b2c7d..05e079ea9155 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 8be75b811b84..c0bb1a885855 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 bdc57c3bd41c..4efe9cca3c55 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 52f5b190c3cb..644039216484 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 24d1236ee4f9..09185b78b6ca 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 b934e344fd1b..9d55b813a6ed 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 f5919c6ccbb8..49a10ad862d0 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 13b93f930b00..e3159fbb1e4d 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 54f2a1b4754d..23f45bb3ff65 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 7501c71b53f0..1a848540d54c 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 f821eebb3fb4..213d4a844e57 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 3a80bbb7a6f2..eb1453a2d604 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 ba8e5cddd47b..df3f02aebf1b 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 4e799829edbf..5210e9293875 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 f875f03bb0e6..e008095a8ea1 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 705756bd039d..45bcd5dbf887 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 37d9932ec1b7..b10b9e19f9da 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 ae99a4dda559..c4f16d565b00 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 396eaddbee49..285950118e40 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 85a230a0a343..34c7ad14bce5 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 c3df6f43b67c..93c1779baca7 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 b3297f6bb000..d53522f88cad 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 1b31ce1ed5e2..f65d4b71a088 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 6a83e820039d..2a857d2d2e1f 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 ceabae916f93..0079a1ebeb59 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 97e3cac9f4ec..3e101f6c4ffd 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 611bc1044fd0..6b22563b3ab0 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/LoginAsCustomerAdminUi/README.md b/app/code/Magento/LoginAsCustomerAdminUi/README.md index 4ae940d51a24..3d447a730140 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 d678d337219f..7da7ee392c03 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/LoginAsCustomerApi/README.md b/app/code/Magento/LoginAsCustomerApi/README.md index af329b244418..39dc0d7bee6e 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 8575763f075b..2fc609f45965 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/LoginAsCustomerFrontendUi/view/frontend/templates/html/notices.phtml b/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/templates/html/notices.phtml index b2e0aaf20ce3..94eaf25e935d 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/templates/html/notices.phtml +++ b/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/templates/html/notices.phtml @@ -8,7 +8,7 @@ * @var \Magento\Framework\View\Element\Template $block * @var \Magento\Framework\Escaper $escaper */ -$viewFileUrl = $block->getViewFileUrl('Magento_LoginAsCustomerFrontendUi::images/magento-icon.svg'); +$viewFileUrl = $block->getViewFileUrl('Magento_LoginAsCustomerFrontendUi::images/mage-os-icon.svg'); ?> <?php if ($block->getConfig()->isEnabled()): ?> <div class="lac-notification-sticky" @@ -17,7 +17,9 @@ $viewFileUrl = $block->getViewFileUrl('Magento_LoginAsCustomerFrontendUi::images <div class="lac-notification clearfix" data-bind="visible: isVisible" style="display: none"> <div class="top-container"> <div class="lac-notification-icon wrapper"> - <img class="logo-img" src="<?= $escaper->escapeUrl($viewFileUrl) ?>" alt="Magento" /> + <img class="logo-img" + src="<?= $escaper->escapeUrl($viewFileUrl) ?>" + alt="<?= $escaper->escapeHtmlAttr(__('Mage-OS')); ?>"/> </div> <div class="lac-notification-text wrapper"> <span data-bind="html: notificationText"></span> diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/web/images/mage-os-icon.svg b/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/web/images/mage-os-icon.svg new file mode 100644 index 000000000000..9921e44b9828 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/web/images/mage-os-icon.svg @@ -0,0 +1,6 @@ +<svg width="160" height="79" viewBox="0 0 160 79" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M53.3902 61.6511L53.3979 30.8256L26.6953 46.2383V77.0639L53.3902 61.6511Z" fill="#FF9234"></path> + <path d="M106.78 61.6511L106.788 30.8256L80.0928 46.2383V77.0639L106.78 61.6511Z" fill="#FF9234"></path> + <path fill-rule="evenodd" clip-rule="evenodd" d="M0 30.8255L53.3976 0L80.0848 15.4128L106.787 0L160.177 30.8255L133.482 46.2383L106.787 30.8255L106.787 30.8257L133.482 46.2383V77.0639L106.779 61.6511L106.787 30.8258L80.0925 46.2383L53.3976 30.8255L53.3974 30.8256L80.0923 46.2383V77.0639L53.3896 61.6511L53.3974 30.8257L26.6949 46.2383L26.6949 77.0639L0 61.6511V30.8255Z" fill="#F37121"></path> + <path d="M160.177 30.8256V61.6511L133.482 77.0639V46.2383L160.177 30.8256Z" fill="#FF9234"></path> +</svg> diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/web/images/magento-icon.svg b/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/web/images/magento-icon.svg deleted file mode 100644 index 47e64067795e..000000000000 --- a/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/web/images/magento-icon.svg +++ /dev/null @@ -1 +0,0 @@ -<svg baseProfile="tiny" xmlns="http://www.w3.org/2000/svg" width="28" height="33" viewBox="-0.154 0 54 62"><g fill="#E85D22"><path d="M26.845 8.857"/><path d="M53.692 15.5v31l-7.67 4.43v-31L26.844 8.856 7.67 19.926V50.93L0 46.5v-31L26.845 0zM26.847 62L15.34 55.355V24.357l7.67-4.43V50.93l3.835 2.327 3.837-2.327v-31l7.67 4.427v30.998z"/></g></svg> diff --git a/app/code/Magento/LoginAsCustomerGraphQl/README.md b/app/code/Magento/LoginAsCustomerGraphQl/README.md index 9e8c7ba71b6c..fa3ff4d8cbcc 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 88d843df2ae0..197a5886e07e 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 c942a830c1dd..36ba12e706b4 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 ac39d72388e6..fc58db1bed37 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 579d7b95fffd..b439491adcf4 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 4571bb956e7a..b07a2f0893d4 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 c3766484ce4f..9136f2454928 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 0fb59f6bb9bc..f77b3392d6c8 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 2ea462cb70e3..cad831f18016 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 3fb2c28f063b..7a553def8aa7 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 e5347f1a1156..622fe7cb2de9 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 b074271149e2..419274a7ecab 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 9f985aa0afa6..fb130449e210 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 5873102dfaa7..afd77836ee2e 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 74d4cf753cb4..96e19a9e9d23 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 3bb56ee256d0..c7a389384e5f 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 668c56baf3ea..b65c70eb5a4e 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 8b5362affc0e..ae9184420c01 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/MediaGalleryCatalogUi/README.md b/app/code/Magento/MediaGalleryCatalogUi/README.md index b26ddf4c8697..e6a9655d4adb 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/MediaGalleryCmsUi/README.md b/app/code/Magento/MediaGalleryCmsUi/README.md index 1152af3c595a..eaa218995ae1 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/MediaGalleryIntegration/README.md b/app/code/Magento/MediaGalleryIntegration/README.md index 676a4eee1cfe..754abc5fbc54 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 08e83ce6cad8..ab25152cf325 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 000000000000..2dabd32eed25 --- /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 ad1dfbf88661..15dd729d2bdd 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 1dc0837ebdad..09ca6117efa8 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 990eff5780c2..51cdd9ed0261 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/etc/media_content.xml b/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml index 7596de07b892..05d2c3006651 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 9c2753aa464c..9c40af6bd5db 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 eebb172e4820..231b7e92f065 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 5937e55b76f6..8c3e631f5eb9 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 82bd1303eda7..9f088dbf2915 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 afeb2b90ec8e..0106cb50f9a0 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 42d3f0cb53e5..6e1fbd199e65 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/Model/Directories/GetDirectoryTree.php b/app/code/Magento/MediaGalleryUi/Model/Directories/GetDirectoryTree.php index 897d0d34a5c8..bff9d9867dd0 100644 --- a/app/code/Magento/MediaGalleryUi/Model/Directories/GetDirectoryTree.php +++ b/app/code/Magento/MediaGalleryUi/Model/Directories/GetDirectoryTree.php @@ -7,7 +7,9 @@ namespace Magento\MediaGalleryUi\Model\Directories; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\ValidatorException; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\Read; @@ -18,6 +20,8 @@ */ class GetDirectoryTree { + private const XML_PATH_MEDIA_GALLERY_IMAGE_FOLDERS + = 'system/media_storage_configuration/allowed_resources/media_gallery_image_folders'; /** * @var Filesystem */ @@ -28,16 +32,24 @@ class GetDirectoryTree */ private $isPathExcluded; + /** + * @var ScopeConfigInterface + */ + private $coreConfig; + /** * @param Filesystem $filesystem * @param IsPathExcludedInterface $isPathExcluded + * @param ScopeConfigInterface|null $coreConfig */ public function __construct( Filesystem $filesystem, - IsPathExcludedInterface $isPathExcluded + IsPathExcludedInterface $isPathExcluded, + ?ScopeConfigInterface $coreConfig = null ) { $this->filesystem = $filesystem; $this->isPathExcluded = $isPathExcluded; + $this->coreConfig = $coreConfig ?? ObjectManager::getInstance()->get(ScopeConfigInterface::class); } /** @@ -74,30 +86,54 @@ private function getDirectories(): array { $directories = []; - /** @var Read $directory */ - $directory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); - - if (!$directory->isDirectory()) { - return $directories; - } - - foreach ($directory->readRecursively() as $path) { - if (!$directory->isDirectory($path) || $this->isPathExcluded->execute($path)) { - continue; + /** @var Read $mediaDirectory */ + $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + + if ($mediaDirectory->isDirectory()) { + $imageFolderPaths = $this->coreConfig->getValue( + self::XML_PATH_MEDIA_GALLERY_IMAGE_FOLDERS, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ); + sort($imageFolderPaths); + + foreach ($imageFolderPaths as $imageFolderPath) { + $imageDirectory = $this->filesystem->getDirectoryReadByPath( + $mediaDirectory->getAbsolutePath($imageFolderPath) + ); + if ($imageDirectory->isDirectory()) { + $directories[] = $this->getDirectoryData($imageFolderPath); + foreach ($imageDirectory->readRecursively() as $path) { + if ($imageDirectory->isDirectory($path)) { + $directories[] = $this->getDirectoryData( + $mediaDirectory->getRelativePath($imageDirectory->getAbsolutePath($path)) + ); + } + } + } } - - $pathArray = explode('/', $path); - $directories[] = [ - 'text' => count($pathArray) > 0 ? end($pathArray) : $path, - 'id' => $path, - 'li_attr' => ['data-id' => $path], - 'path' => $path, - 'path_array' => $pathArray - ]; } + return $directories; } + /** + * Return jstree data for given path + * + * @param string $path + * @return array + */ + private function getDirectoryData(string $path): array + { + $pathArray = explode('/', $path); + return [ + 'text' => count($pathArray) > 0 ? end($pathArray) : $path, + 'id' => $path, + 'li_attr' => ['data-id' => $path], + 'path' => $path, + 'path_array' => $pathArray + ]; + } + /** * Find parent directory * @@ -121,9 +157,9 @@ private function findParent(array &$node, array &$treeNode, int $level = 0): arr $tNodePathLength = count($tnode['path_array']); $found = false; while ($level < $tNodePathLength) { - if ($node['path_array'][$level] === $tnode['path_array'][$level]) { + $found = $node['path_array'][$level] === $tnode['path_array'][$level]; + if ($found) { $level ++; - $found = true; } else { break; } diff --git a/app/code/Magento/MediaGalleryUi/README.md b/app/code/Magento/MediaGalleryUi/README.md index 1a6fc0f4b235..c1dc448bc799 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 000000000000..a3645661e55f --- /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 8b0c984c1df7..139fb1021f32 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 91478877cfe5..3873d638f5fa 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/Unit/Model/Model/Directories/GetDirectoryTreeTest.php b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/Model/Directories/GetDirectoryTreeTest.php new file mode 100644 index 000000000000..df7647a66da5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/Model/Directories/GetDirectoryTreeTest.php @@ -0,0 +1,304 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Test\Unit\Model\Model\Directories; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; +use Magento\MediaGalleryUi\Model\Directories\GetDirectoryTree; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class GetDirectoryTreeTest extends TestCase +{ + /** + * @var Filesystem|MockObject + */ + private $filesystem; + + /** + * @var IsPathExcludedInterface|MockObject + */ + private $isPathExcluded; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $coreConfig; + + /** + * @var GetDirectoryTree + */ + private $model; + + /** + * @var array + */ + private $foldersStruture = [ + 'dir1' => [ + 'dir1_1' => [ + + ], + 'dir1_2' => [ + + ], + 'dir1_3' => [ + + ] + ], + 'dir2' => [ + 'dir2_1' => [ + 'dir2_1_1' => [ + + ] + ], + 'dir2_2' => [ + 'dir2_2_1' => [ + + ], + 'dir2_2_2' => [ + + ] + ] + ], + 'dir3' => [ + 'dir3_1' => [ + 'dir3_1_1' => [ + 'dir3_1_1_1' => [ + + ] + ] + ] + ], + 'dir4' => [ + + ], + ]; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->filesystem = $this->createMock(Filesystem::class); + $this->isPathExcluded = $this->getMockForAbstractClass(IsPathExcludedInterface::class); + $this->coreConfig = $this->getMockForAbstractClass(ScopeConfigInterface::class); + $this->model = new GetDirectoryTree( + $this->filesystem, + $this->isPathExcluded, + $this->coreConfig + ); + } + + /** + * @param array $allowedFolders + * @param array $expected + * @throws ValidatorException + * @dataProvider executeDataProvider + */ + public function testExecute(array $allowedFolders, array $expected): void + { + $directory = $this->getMockForAbstractClass(ReadInterface::class); + $directory->method('isDirectory')->willReturn(true); + $directory->method('getAbsolutePath')->willReturnArgument(0); + $directory->method('getRelativePath')->willReturnArgument(0); + $this->filesystem->method('getDirectoryRead')->willReturn($directory); + $this->filesystem->method('getDirectoryReadByPath') + ->willReturnCallback( + function (string $path) { + $directory = $this->getMockBuilder(ReadInterface::class) + ->addMethods(['readRecursively']) + ->getMockForAbstractClass(); + $directory->method('isDirectory')->willReturn(true); + $result = $this->foldersStruture; + $prefix = ''; + foreach (explode('/', $path) as $folder) { + $prefix .= $folder . '/'; + $result = $result[$folder] ?? []; + } + $directory->method('getAbsolutePath')->willReturnArgument(0); + $directory->method('readRecursively')->willReturn($this->flattenFoldersStructure($result, $prefix)); + return $directory; + } + ); + $this->coreConfig->method('getValue')->willReturn($allowedFolders); + $this->assertEquals($expected, $this->model->execute()); + } + + /** + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function executeDataProvider(): array + { + return [ + [ + ['dir1/dir1_1', 'dir2/dir2_2', 'dir3'], + [ + [ + 'text' => 'dir1_1', + 'id' => 'dir1/dir1_1', + 'li_attr' => ['data-id' => 'dir1/dir1_1'], + 'path' => 'dir1/dir1_1', + 'path_array' => ['dir1', 'dir1_1'], + 'children' => [], + ], + [ + 'text' => 'dir2_2', + 'id' => 'dir2/dir2_2', + 'li_attr' => ['data-id' => 'dir2/dir2_2'], + 'path' => 'dir2/dir2_2', + 'path_array' => ['dir2', 'dir2_2'], + 'children' => + [ + [ + 'text' => 'dir2_2_1', + 'id' => 'dir2/dir2_2/dir2_2_1', + 'li_attr' => + [ + 'data-id' => 'dir2/dir2_2/dir2_2_1', + ], + 'path' => 'dir2/dir2_2/dir2_2_1', + 'path_array' => ['dir2', 'dir2_2', 'dir2_2_1'], + 'children' => [], + ], + [ + 'text' => 'dir2_2_2', + 'id' => 'dir2/dir2_2/dir2_2_2', + 'li_attr' => ['data-id' => 'dir2/dir2_2/dir2_2_2'], + 'path' => 'dir2/dir2_2/dir2_2_2', + 'path_array' => ['dir2', 'dir2_2', 'dir2_2_2'], + 'children' => [], + ], + ], + ], + [ + 'text' => 'dir3', + 'id' => 'dir3', + 'li_attr' => ['data-id' => 'dir3'], + 'path' => 'dir3', + 'path_array' => ['dir3'], + 'children' => + [ + [ + 'text' => 'dir3_1', + 'id' => 'dir3/dir3_1', + 'li_attr' => ['data-id' => 'dir3/dir3_1'], + 'path' => 'dir3/dir3_1', + 'path_array' => ['dir3', 'dir3_1'], + 'children' => + [ + [ + 'text' => 'dir3_1_1', + 'id' => 'dir3/dir3_1/dir3_1_1', + 'li_attr' => ['data-id' => 'dir3/dir3_1/dir3_1_1'], + 'path' => 'dir3/dir3_1/dir3_1_1', + 'path_array' => ['dir3', 'dir3_1', 'dir3_1_1'], + 'children' => + [ + [ + 'text' => 'dir3_1_1_1', + 'id' => 'dir3/dir3_1/dir3_1_1/dir3_1_1_1', + 'li_attr' => [ + 'data-id' => 'dir3/dir3_1/dir3_1_1/dir3_1_1_1', + ], + 'path' => 'dir3/dir3_1/dir3_1_1/dir3_1_1_1', + 'path_array' => [ + 'dir3', + 'dir3_1', + 'dir3_1_1', + 'dir3_1_1_1', + ], + 'children' => [], + ], + ], + ], + ], + ] + ], + ], + ] + + ], + [ + ['dir2/dir2_1', 'dir2/dir2_2'], + [ + [ + 'text' => 'dir2_1', + 'id' => 'dir2/dir2_1', + 'li_attr' => ['data-id' => 'dir2/dir2_1'], + 'path' => 'dir2/dir2_1', + 'path_array' => ['dir2', 'dir2_1'], + 'children' => + [ + [ + 'text' => 'dir2_1_1', + 'id' => 'dir2/dir2_1/dir2_1_1', + 'li_attr' => + [ + 'data-id' => 'dir2/dir2_1/dir2_1_1', + ], + 'path' => 'dir2/dir2_1/dir2_1_1', + 'path_array' => ['dir2', 'dir2_1', 'dir2_1_1'], + 'children' => [], + ] + ], + ], + [ + 'text' => 'dir2_2', + 'id' => 'dir2/dir2_2', + 'li_attr' => ['data-id' => 'dir2/dir2_2'], + 'path' => 'dir2/dir2_2', + 'path_array' => ['dir2', 'dir2_2'], + 'children' => + [ + [ + 'text' => 'dir2_2_1', + 'id' => 'dir2/dir2_2/dir2_2_1', + 'li_attr' => + [ + 'data-id' => 'dir2/dir2_2/dir2_2_1', + ], + 'path' => 'dir2/dir2_2/dir2_2_1', + 'path_array' => ['dir2', 'dir2_2', 'dir2_2_1'], + 'children' => [], + ], + [ + 'text' => 'dir2_2_2', + 'id' => 'dir2/dir2_2/dir2_2_2', + 'li_attr' => ['data-id' => 'dir2/dir2_2/dir2_2_2'], + 'path' => 'dir2/dir2_2/dir2_2_2', + 'path_array' => ['dir2', 'dir2_2', 'dir2_2_2'], + 'children' => [], + ], + ], + ] + ] + ] + ]; + } + + /** + * @param array $array + * @param string $prefix + * @return array + */ + private function flattenFoldersStructure(array $array, string $prefix = ''): array + { + $paths = []; + foreach ($array as $key => $value) { + $path = $prefix . $key; + $paths[] = [$path]; + $paths[] = $this->flattenFoldersStructure($value, $path . '/'); + } + return array_merge(...$paths); + } +} diff --git a/app/code/Magento/MediaGalleryUi/etc/config.xml b/app/code/Magento/MediaGalleryUi/etc/config.xml index fe8e73c406e5..593b1b8e58fd 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 12e63b5a0095..585428276f13 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 6b022e18a796..e79e68a82c85 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 9a74cf4ce842..3e401c7aa605 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 000000000000..b12bcb120ed4 --- /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 5cdcbb3b2b9a..db03601835fd 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 000000000000..320e5af8e022 --- /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 c097f461e621..49540e248319 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 000000000000..a92a8e82e195 --- /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 000000000000..44099f6fe357 --- /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 000000000000..15a4a397bb11 --- /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 000000000000..2ea95960ae8e --- /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 b283280dc458..caee6f7820c3 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 025b215d285a..a82a5b391c5d 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 874edf0dff9e..941ede7c3538 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 86732ba1e18b..2bb27f8ac948 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 5ef73f4dfed4..de8e4e7bbdc0 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 42bf5772e96e..bf54b46a717d 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 f3f24170c944..de3160ad7c51 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 800bf0eedd74..605ca4714a0b 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 12bda8ae5f21..2e1c88dc1818 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 000000000000..a31fc9a0e02f --- /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 000000000000..012648deae5e --- /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 000000000000..c401098a0794 --- /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 304f0a9c7a12..5273a56bb9ed 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 ef41ed3f47f3..af62a0fc2833 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 cf6bd10b0e8d..279967c2a3be 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 6cfc09c1653f..c2ae07b98797 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 39ad54fc6671..1306f5bcca99 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 e7058304f860..919ea359e2f7 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 000000000000..87c579dc0cf9 --- /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 618c32b21ad0..499287c1efba 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 e22df0a8f306..3e596ece69f0 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 084e0ffc9f3a..185cf11cec0a 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 82563e5055c2..07b2834fa04d 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 f05c6e355bb1..73b02e30c3d8 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 b051e9622b3b..9dc3ad5bfe15 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 8108de8f9e2d..19d3d384e0d1 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 815d406c68bf..339459f66f2b 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 a0f000b6abd5..8890b15dfab1 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 1d9b6e99a1ea..6f51cdcec6b3 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 8c0df3c70677..ec57c3764290 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 8205ab962b9f..b54385fd610f 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 065d435b9e43..daabc17ea408 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 632950120474..cb34bbfe548c 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 f0a97d240aa6..5f5f118a7e99 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 93bce523832a..2218c4463a54 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 d3bc1e13222d..b8f38abba089 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 79d2a6942e6d..91e32a65a46d 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 d072cafd8aa5..54bfbcad0b9d 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 cdf9c5683c57..961e0b6fed86 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 4377b8cfd8c1..7d462c7bc5d3 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 30e5d360f430..3ef97f33a1ad 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/MysqlMq/Model/Driver/Queue.php b/app/code/Magento/MysqlMq/Model/Driver/Queue.php index cbc2e951782f..6d29fc8aee57 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 2a45eafc63f2..a110f1efdd0c 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 5f41956aee4c..9da1e54fd787 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 2b9013d59470..d1013c454a17 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 20cee6087e6e..61a4c099c5f7 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 90aca4eb8529..a2cebb0ee45f 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 3be9d2d8445d..a202dcf23a29 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 ae632441f107..d64458fe8e76 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 cd8b0f46087a..4b03aecac6ff 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 ca90b5d84a10..d60c567073d6 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 55a6509327ec..46522450c272 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 37abccea93b8..4924a6a70089 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 76fe12658f75..30c4ff598247 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 f4e72c61953f..bd7c62e82e63 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 64f6c066f01b..d59bf2831077 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 053640751b71..b51cc7508d3f 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 000000000000..d58358cbf089 --- /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 fd9c37238868..dd2def1f5d91 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 0341e68cf2b1..3890eb708c6b 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 05ad191360b3..8ca56805c1a7 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 44104f3adf0d..e2954fcbb6f9 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 9248970d9e62..77da0cd36395 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 1f0cf68d0f5e..139d14ed6f89 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 b12629a666af..ddd6b8ab1694 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 c472d262a34c..5e6c618debcf 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 d6bee2c61884..34c68efb05a0 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 0db48811e802..f02e0f61a3da 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 93429bfd14f8..012270cf1c63 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 5e35f5aab60c..ec07415e4a76 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 746c786ef3da..7e9c52f183a2 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 da46d79185f2..26de05d36d7a 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 7ec1fb62fc00..3c67f35c99e7 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 1b56f1204997..c07a7e5eafb1 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 a3b2a5f93a12..cdbab53b6578 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/view/frontend/layout/default.xml b/app/code/Magento/Newsletter/view/frontend/layout/default.xml index 32a08359333c..6a2835862096 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 768c97ef316f..554cc4e16bd6 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 e897c4838284..c8e0121e47a0 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 302a562ec470..814913202f1a 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 7c2d5ca6b26b..bd1323b5c493 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 9aec95f6e02f..8b34b3f2c499 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 000000000000..f25b23f8b9d9 --- /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 ec8dd46a00d8..a6510a5ae526 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 000000000000..d8258bfbc6d4 --- /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 d9a2f89cead8..000000000000 --- 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 041adfa3fb35..70723ba5b6d4 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 7735f8ce8999..5ce3e8fc43e3 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 08213d608536..440f2bc3e154 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 d225e5fa28f9..fb3b950cd2af 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 000000000000..ae55756990c3 --- /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 000000000000..e27c35a71c11 --- /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 8d75ed5d524b..000000000000 --- 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 c2b1b161e552..0b95a18edcca 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 9e75d64075f8..a66d489b0b3e 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 000000000000..d88e01472684 --- /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 bc1f4f7473f4..e50eb44b96e3 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/Model/OpenSearch.php b/app/code/Magento/OpenSearch/Model/OpenSearch.php new file mode 100644 index 000000000000..b658e0012a26 --- /dev/null +++ b/app/code/Magento/OpenSearch/Model/OpenSearch.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\OpenSearch\Model; + +/** + * The purpose of this class is adding the support for opensearch version 2 + */ + +class OpenSearch extends SearchClient +{ + + /** + * Add mapping to OpenSearch index + * + * @param array $fields + * @param string $index + * @param string $entityType + * @return void + */ + public function addFieldsMapping(array $fields, string $index, string $entityType) + { + $params = [ + 'index' => $index, + 'body' => [ + 'properties' => [], + 'dynamic_templates' => $this->dynamicTemplatesProvider->getTemplates(), + ], + ]; + + foreach ($this->applyFieldsMappingPreprocessors($fields) as $field => $fieldInfo) { + $params['body']['properties'][$field] = $fieldInfo; + } + + $this->getOpenSearchClient()->indices()->putMapping($params); + } + + /** + * Execute search by $query + * + * @param array $query + * @return array + */ + public function query(array $query): array + { + unset($query['type']); + return $this->getOpenSearchClient()->search($query); + } +} diff --git a/app/code/Magento/OpenSearch/Model/SearchClient.php b/app/code/Magento/OpenSearch/Model/SearchClient.php index feb6f952a20f..dbce79c2496b 100644 --- a/app/code/Magento/OpenSearch/Model/SearchClient.php +++ b/app/code/Magento/OpenSearch/Model/SearchClient.php @@ -42,7 +42,7 @@ class SearchClient implements ClientInterface /** * @var DynamicTemplatesProvider|null */ - private $dynamicTemplatesProvider; + public $dynamicTemplatesProvider; /** * Initialize Client @@ -93,7 +93,7 @@ public function suggest(array $query): array * * @return Client */ - private function getOpenSearchClient(): Client + public function getOpenSearchClient(): Client { $pid = getmypid(); if (!isset($this->client[$pid])) { @@ -371,7 +371,7 @@ public function deleteMapping(string $index, string $entityType) * @param array $properties * @return array */ - private function applyFieldsMappingPreprocessors(array $properties): array + public function applyFieldsMappingPreprocessors(array $properties): array { foreach ($this->fieldsMappingPreprocessors as $preprocessor) { $properties = $preprocessor->process($properties); diff --git a/app/code/Magento/OpenSearch/README.md b/app/code/Magento/OpenSearch/README.md index 36ab12072d57..eeeffcd968ef 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 bbe9846116bc..2db420995224 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 new file mode 100644 index 000000000000..74c67dfd2632 --- /dev/null +++ b/app/code/Magento/OpenSearch/Test/Mftf/Test/OpenSearchUpgradeVersion2xTest.xml @@ -0,0 +1,52 @@ +<?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="OpenSearchUpgradeVersion2xTest"> + <annotations> + <stories value="Update OpenSearch to the v2.x"/> + <title value="Upgrading the version of opensearch to 2x"/> + <description value="Upgrading the version of opensearch to 2x"/> + <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"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct01"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct02"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <createData entity="AssignProductToCategory" stepKey="assignTestCategoryToTestProduct"> + <requiredEntity createDataKey="createCategory"/> + <requiredEntity createDataKey="createProduct01"/> + <requiredEntity createDataKey="createProduct02"/> + </createData> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct01" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct02" stepKey="deleteProduct2"/> + </after> + + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="goToStorefrontCreatedCategoryPage"/> + <wait time="2" stepKey="waitForLoad"/> + </test> +</tests> 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 016675da2ce9..dbcc8c2bb06e 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/Test/Unit/Model/OpenSearchTest.php b/app/code/Magento/OpenSearch/Test/Unit/Model/OpenSearchTest.php new file mode 100644 index 000000000000..5fa55dcab9ca --- /dev/null +++ b/app/code/Magento/OpenSearch/Test/Unit/Model/OpenSearchTest.php @@ -0,0 +1,201 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\OpenSearch\Test\Unit\Model; + +use OpenSearch\Client; +use OpenSearch\Namespaces\IndicesNamespace; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\AddDefaultSearchField; +use Magento\OpenSearch\Model\Adapter\DynamicTemplates\IntegerMapper; +use Magento\OpenSearch\Model\Adapter\DynamicTemplates\PositionMapper; +use Magento\OpenSearch\Model\Adapter\DynamicTemplates\PriceMapper; +use Magento\OpenSearch\Model\Adapter\DynamicTemplates\StringMapper; +use Magento\OpenSearch\Model\Adapter\DynamicTemplatesProvider; +use Magento\OpenSearch\Model\OpenSearch; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class OpensearchTest to test OpensearchTest2x + */ +class OpenSearchTest extends TestCase +{ + /** + * @var OpenSearch + */ + private $model; + + /** + * @var Client|MockObject + */ + private $opensearchV2ClientMock; + + /** + * @var IndicesNamespace|MockObject + */ + private $indicesMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManager; + + /** + * Setup + * + * @return void + */ + protected function setUp(): void + { + $this->opensearchV2ClientMock = $this->getMockBuilder(Client::class) + ->setMethods( + [ + 'indices', + 'search' + ] + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->indicesMock = $this->getMockBuilder(IndicesNamespace::class) + ->setMethods( + [ + 'putMapping' + ] + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->opensearchV2ClientMock->expects($this->any()) + ->method('indices') + ->willReturn($this->indicesMock); + + $this->objectManager = new ObjectManagerHelper($this); + $dynamicTemplatesProvider = new DynamicTemplatesProvider( + [ new PriceMapper(), new PositionMapper(), new StringMapper(), new IntegerMapper()] + ); + $this->model = $this->objectManager->getObject( + OpenSearch::class, + [ + 'options' => $this->getOptions(), + 'openSearchClient' => $this->opensearchV2ClientMock, + 'fieldsMappingPreprocessors' => [new AddDefaultSearchField()], + 'dynamicTemplatesProvider' => $dynamicTemplatesProvider, + ] + ); + } + + /** + * Test query() method + * + * @return void + */ + public function testQuery() + { + $query = ['test phrase query']; + $this->opensearchV2ClientMock->expects($this->once()) + ->method('search') + ->with($query) + ->willReturn([]); + $this->assertEquals([], $this->model->query($query)); + } + /** + * Get client options + * + * @return array + */ + protected function getOptions() + { + return [ + 'hostname' => 'localhost', + 'port' => '9200', + 'timeout' => 15, + 'index' => 'magento2', + 'enableAuth' => 1, + 'username' => 'user', + 'password' => 'passwd', + ]; + } + + /** + * Test testAddFieldsMapping() method + */ + public function testAddFieldsMapping() + { + $this->indicesMock->expects($this->once()) + ->method('putMapping') + ->with( + [ + 'index' => 'indexName', + 'body' => [ + 'properties' => [ + '_search' => [ + 'type' => 'text', + ], + '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, + 'copy_to' => '_search', + ], + ], + ], + [ + 'integer_mapping' => [ + 'match_mapping_type' => 'long', + 'mapping' => [ + 'type' => 'integer', + ], + ], + ], + ], + ], + ] + ); + $this->model->addFieldsMapping( + [ + 'name' => [ + 'type' => 'text', + ], + ], + 'indexName', + 'product' + ); + } +} diff --git a/app/code/Magento/OpenSearch/composer.json b/app/code/Magento/OpenSearch/composer.json index 254230ee4dc0..1b9e006b9e9b 100644 --- a/app/code/Magento/OpenSearch/composer.json +++ b/app/code/Magento/OpenSearch/composer.json @@ -9,7 +9,7 @@ "magento/module-elasticsearch": "*", "magento/module-search": "*", "magento/module-config": "*", - "opensearch-project/opensearch-php": "~1.0.0" + "opensearch-project/opensearch-php": "^1.0 || ^2.0" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/OpenSearch/etc/adminhtml/system.xml b/app/code/Magento/OpenSearch/etc/adminhtml/system.xml index 56d9eff92fd3..45409dee7058 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 65d4ddb7dc54..62e47ddec45b 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> @@ -75,9 +75,10 @@ <virtualType name="Magento\OpenSearch\Model\Client\OpenSearchFactory" type="Magento\AdvancedSearch\Model\Client\ClientFactory"> <arguments> <argument name="clientClass" xsi:type="string">Magento\OpenSearch\Model\SearchClient</argument> + <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> @@ -113,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> @@ -129,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 000000000000..e3ecd99b2f9d --- /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 000000000000..4ae5619d8e9a --- /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 000000000000..2a413c9d3309 --- /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 000000000000..57f78a2192b9 --- /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 000000000000..0767523f6666 --- /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 000000000000..b3af3df5f946 --- /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 000000000000..e7d871745d6f --- /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 000000000000..b875735c4009 --- /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 000000000000..6c79eefce2b2 --- /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 000000000000..f6aa3a1f4ddd --- /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 000000000000..4db3c8a29625 --- /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 000000000000..bb9120580ac9 --- /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 000000000000..a3a12fd58bcb --- /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 000000000000..203189674920 --- /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 000000000000..e64b987c87e3 --- /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 000000000000..02461e0f448f --- /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 000000000000..ebb1cc3fa762 --- /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 000000000000..83ed1e46247e --- /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 000000000000..901d580188db --- /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 000000000000..712a0cb0da46 --- /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 000000000000..7457494dd0f7 --- /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 000000000000..139bc65dd9ef --- /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 000000000000..d3f4689314b9 --- /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 000000000000..bbe85edbd5ae --- /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 000000000000..4d7ebb22934b --- /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 000000000000..f4c1edbe7b68 --- /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 000000000000..ceac9d2adc9c --- /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 000000000000..83e04f9a9ebd --- /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 000000000000..6ca06293f3d3 --- /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 000000000000..27e375f5875a --- /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 000000000000..d2db99cc37fa --- /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 000000000000..2fd084798c62 --- /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 000000000000..ad7fd65c6af6 --- /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 000000000000..bb9af1337f41 --- /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 000000000000..2135d24a39a8 --- /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 000000000000..cdfb14d62548 --- /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 000000000000..13555cf9bfbd --- /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 000000000000..bc8de00c9a00 --- /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 000000000000..99653ef23bd4 --- /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 000000000000..e01b636fc484 --- /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 000000000000..883642d611a2 --- /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 000000000000..4b2420643ba4 --- /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 000000000000..e976c3fd4573 --- /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 000000000000..9ae4f5a3cfbe --- /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 000000000000..ddd9a46eecb1 --- /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 000000000000..97a2beb6546d --- /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 000000000000..20b9f13ac4ff --- /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 000000000000..780e9b62573d --- /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 000000000000..9e90a62f3222 --- /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 000000000000..0107268de5a5 --- /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 000000000000..d9687a595962 --- /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 000000000000..46daf397549f --- /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 000000000000..5ba4ba97e7d6 --- /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 000000000000..eff23b989a42 --- /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 e69614496c66..b32866524d9d 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 5340f5204e21..061cc801d5d1 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 000000000000..26b8715c3644 --- /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 1b109926fd9f..30e46cb560d5 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 eeac1c2fe112..5851c8dbac5c 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 a7cf367ff303..4d1a17463266 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 c0c4eac7d525..a003d6aa3bd1 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 89e4b06994a7..7cec177a3a0b 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 0827f84a2119..30e0e6a0276a 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 000000000000..4a9b884e6c5c --- /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 1aaa331da702..7f4d05ae206b 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 0f67aae1e697..7622025cc296 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 bd9e5c92f507..335ffe289e72 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 16dd9505e834..ee89dc8d22d7 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 b62e86e0f831..f52fb081cd7d 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 44f008db574a..5996844ebec5 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 7808f4cb4af6..000000000000 --- 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/Unit/Plugin/PaymentMethodProcessTest.php b/app/code/Magento/Payment/Test/Unit/Plugin/PaymentMethodProcessTest.php deleted file mode 100644 index 9a08f47727bb..000000000000 --- 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 36cd77ea50d4..7d986543ef60 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 a826dedf9f02..b7422bb00d54 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/Payment/view/base/web/js/model/credit-card-validation/credit-card-number-validator/credit-card-type.js b/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/credit-card-number-validator/credit-card-type.js index 1b387b384104..e52ce3b5679d 100644 --- a/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/credit-card-number-validator/credit-card-type.js +++ b/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/credit-card-number-validator/credit-card-type.js @@ -103,7 +103,7 @@ define([ { title: 'Maestro Domestic', type: 'MD', - pattern: '^6759(?!24|38|40|6[3-9]|70|76)|676770|676774\\d*$', + pattern: '^(6759(?!24|38|40|6[3-9]|70|76)|676770|676774)\\d*$', gaps: [4, 8, 12], lengths: [12, 13, 14, 15, 16, 17, 18, 19], code: { diff --git a/app/code/Magento/Paypal/Model/Cart.php b/app/code/Magento/Paypal/Model/Cart.php index 9ebf6ed41fd9..49f50f834c42 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 438ec0f0235d..c638e7427971 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 b15e8441cf73..05f90295cb7e 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 705b667ab2f6..53c4a4e08318 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 0ed4f2e90291..555449257de5 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 b4ce8c9c7637..ad189302b120 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 000000000000..4e7a4df7d38f --- /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 000000000000..c97e580a2c93 --- /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 000000000000..03f05edb1f74 --- /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 000000000000..2bbea0be7e59 --- /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 b653858f770e..e20b38638cad 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 a2c7b7d82a34..0a1077e0c18e 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 95e69cf6e93c..4e88bbe73e2e 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 000000000000..9f4b2a6a47f1 --- /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 000000000000..90f495560c93 --- /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 feb889ec7660..7fa736ffdb25 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 000000000000..c0cf2e0fc353 --- /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 3b70bc84037c..c66df74869e5 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 038ee1c04c48..8286f30fd515 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 ad24d2c2c95d..561c98131c74 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 846f4e6dd5ae..7c7a2e6100f1 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 b0317f9ac7a3..05ae84d70d3a 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 a616c0bb2c68..2af6a73cf1a1 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 778473abb2cc..b46565fcc99f 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 3bd778620f56..f66d2bae639d 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 000000000000..2af6cf395b7f --- /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 000000000000..22f79cb99a07 --- /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 000000000000..638363364b56 --- /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 000000000000..e4e07d83b8df --- /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 7fbb1ea62353..0762b9ae6ade 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 26d9387a0c35..2f2025bfd645 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 6fa30ac29c6d..34f93773b0c6 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 62720f2f3ae0..00d91fd00a42 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 baa4bf39e678..aae9e39cc3b2 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/Unit/Controller/ExpressTest.php b/app/code/Magento/Paypal/Test/Unit/Controller/ExpressTest.php index c164d832ad46..25f99bf7acc3 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 affb335491c5..b2179fb32fe5 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 8fc8bc72ed59..4befeba8ef52 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 1fb71dbc5ac3..6fa178395ae8 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 0bda87ded5d7..c4dbe01d1938 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 4af05ea3fca5..acc25198a19c 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 71588599a5ec..02b1cd3c3f93 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 4f23433147be..b73e0b746518 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/Persistent/Model/Plugin/LoginAsCustomerCleanUp.php b/app/code/Magento/Persistent/Model/Plugin/LoginAsCustomerCleanUp.php new file mode 100644 index 000000000000..4611cc5f0487 --- /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 d3f015bf29d5..3d2f19e4fc91 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 1f944432ac1d..e6d701c01e47 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 f094c4f07475..16275ec595b2 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 ebc3aee9d2fd..fae81091db23 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 daf25eb0dff2..e47ab9187010 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 159b5b6b9e79..94bd1459d268 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 0a866cd0cfa6..c76af56e71b5 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/Unit/Model/Plugin/LoginAsCustomerCleanUpTest.php b/app/code/Magento/Persistent/Test/Unit/Model/Plugin/LoginAsCustomerCleanUpTest.php new file mode 100644 index 000000000000..43519f362f59 --- /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 5a8ff5d7f3d5..6c943c4b37f8 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 335196323127..498b59b7e4c4 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 83ac4abd896c..a77ca851ac74 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 27a747d6ed4c..1d54f5e7b811 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 000000000000..c9b8e331ad7a --- /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 000000000000..09d47ddc2b1a --- /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 ba75415d095a..562ad8c64396 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 76a8036e9c3c..f3b9926dd111 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 5b346040db81..d3ce3159187c 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 000000000000..3086ee1979f1 --- /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 bfb1be1f978b..9facf079ff4d 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 670d91febe9f..a8a168c03aa9 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 ee122d1b02ff..dc0ce80f74dd 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 f1ee8bd83fe9..048636697592 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 b00a6617beae..e992fab92554 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 9b1655251db7..0d3c5dbcad9b 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 000000000000..10286d8453c8 --- /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 000000000000..82df3ac0beb0 --- /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 000000000000..da80bd96f708 --- /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 000000000000..e37504b0ac84 --- /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 000000000000..09b6ea3cd555 --- /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 6f8a44dff464..9ed4f5ecd516 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 6a333e8b9b79..1dd127977d68 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 ab49c31179c0..fcaa299fd087 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 572d87d5f4be..7270734e3ff4 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 2d3c072d5d88..c759266d2e69 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 6fdb70350ed7..9f28f52adee9 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 3f96be4bd25a..339add647c90 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 000000000000..3536e092d132 --- /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 11849bb2447b..a4b0ecf5c442 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 abe8b0d96605..f7fa741f0f1c 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 5bc1d92f4403..d958d1f4d143 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 f0bc12f7b3a3..f8e7141b3bb0 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 dc0858f18380..aada74198268 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 b1bef834197a..776479a4773f 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 12a71648690d..12d155de5b01 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 b59737bff988..6e27625283be 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 a40884aa98e0..439b902ca994 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 000000000000..ab6bf0a3ff89 --- /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 b81f85ae064f..49628d233556 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 000000000000..93943b8eae76 --- /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 000000000000..b38072d40fa7 --- /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 7dd0bcf8f8b0..5a8905f8b98d 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 ead52c61bcb1..7de4620c05c6 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 6fc54f43c63f..044102ca5a18 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 c547e11c1635..c2be964b4eee 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 ff183e315089..6213497833a1 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 5667a9a5b460..9e1f8ce164b6 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 5ffc82d05e20..496996d77541 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 d96c88b7795f..483b29a9fdbc 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 d25faa5bd322..e9e220549ab4 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 8e9864a46142..f4df89c6a8ab 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 8360f10a355a..31d75f1cd897 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 83c74e5f52bf..56184244bfbc 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 000000000000..45dea83df88a --- /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 e15b7324ce24..7fdce5245b47 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 000000000000..af52c2869e90 --- /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 ed08d60f3f3b..c785b632c000 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 82cbd8cbfde2..645e4eb35c54 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 2b13086fc7a2..000000000000 --- 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 6e4c85d1a2f6..000000000000 --- 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 06fc3ad2e665..e57deafe9c53 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 85e744c026c4..b0d68aa63439 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 000000000000..278239bba54f --- /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 13636e46651b..006c105661fe 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 dfbc20bf7abd..4722b3db537a 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 d0c69b8b5449..1f7e0f914d7c 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 755f79569f09..c607c77659dc 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 7cbc64a41d37..e77894a3eef4 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 09ef1ad58187..307087391b89 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 b20bbe0e0066..01e9a95dd5c7 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 bcacd58fcb7e..eeed8e84d8ef 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 d5cc67234308..7eebc7c5e529 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 000000000000..df11ad35d068 --- /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 000000000000..68f52c4a348d --- /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 24cb1382634c..62c37801cbcb 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 63eb001821c0..b7a4130a7114 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 218806270ab9..1b65bdcf4564 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 e5084d4c9f9b..1dec3387c87f 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 fac7b23d408e..f35af6f4885d 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 62cd55df8559..d25286e686ae 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 46d56107f2aa..c6e55ab091bc 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 178482dc7a98..50a4587b4398 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 9c6d152bed27..16b7b94da858 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 e1fda91923e4..f67eee4ddb0c 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 4593c2628155..ae0dc791c275 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/RemoteStorage/composer.json b/app/code/Magento/RemoteStorage/composer.json index 7b778045af7a..107ddf6788fe 100644 --- a/app/code/Magento/RemoteStorage/composer.json +++ b/app/code/Magento/RemoteStorage/composer.json @@ -4,8 +4,8 @@ "require": { "php": "~8.1.0||~8.2.0", "magento/framework": "*", - "league/flysystem": "~2.4.3", - "league/flysystem-aws-s3-v3": "^2.4.3" + "league/flysystem": "^2.4", + "league/flysystem-aws-s3-v3": "^2.4" }, "suggest": { "magento/module-backend": "*", 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 97d0493c5d9d..57006ff6c9bf 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 5a92b6ab4e79..2fb1ab26ac07 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 7ace2e63f1a5..f932c61cb435 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 c6c6a78c7b8b..7a87d0833f08 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 67e451c4c591..736733a2f980 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 b987e09194b3..26b63ce2cb63 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 1fac1a15782c..3d90323a3ee5 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 000000000000..8e22305f8da8 --- /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 000000000000..901cb1282118 --- /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 000000000000..851463da9a55 --- /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 000000000000..8da8e84bda19 --- /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 000000000000..b03942efc376 --- /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 000000000000..4ddbef1150ac --- /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 000000000000..873988e34e0c --- /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 980904ba0818..43c704137ef6 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 600291dffade..b9d1f3a0704f 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 88c94a27a83f..f550bf6d58b1 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 000000000000..14fb0ef3074e --- /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 efe988acbcf7..d4ae4752e93a 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 14db012e7688..c753ff49a0ff 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 d6a3ae8bcd20..261ff3c9e537 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 e9ed4caa7ef0..c99c6faf7c1e 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 7756a43c68ac..cad8185fe204 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 8d8ceb69ba7b..d2de9cefd4f5 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 30d9392071a9..5d4e5c494f7a 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 cb17169c4cf8..30980009d847 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 c3d0f3b51f69..313770e7411d 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 fb44eca668e6..04babc57ef4c 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 d42a41a45d6f..51c3148bcb48 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 388be2711307..6595baa826eb 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 ec8b60ac743f..601c2015ea7e 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 9a32e20594df..3d0a247a0b18 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 f8091d4f6310..65fb97d60fa7 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 d68eb332d81a..45ba0e76f67a 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 000000000000..ed290b685340 --- /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 6857b5af3397..6fa28a49a64d 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/Unit/Block/Adminhtml/Grid/Column/Renderer/CurrencyTest.php b/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/Column/Renderer/CurrencyTest.php index e55cda299682..bc5a40302bf2 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 9e4f39be6b7d..f2d6433ec24b 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 8ed9f8809560..55573d9c5faf 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 5f739b259541..fca6419ca603 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 efffa7a02678..452f71680029 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 b042def8dac7..76158272baee 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 83108ad6cb51..535dea3da46b 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 d3bbdf9a7eb4..282df148991a 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 a9b6c8917628..af5e2cfe088d 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 61bb2ce2e903..3051039abae2 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 86c7ee6a2459..6d894a0b4480 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 bb322f17b6ce..803b07304062 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 c6dfad8265ac..35a47aca8715 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 e76fa8bd1c6d..cedb01e063e2 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 569105f60490..a8b32212b7aa 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 c66e3e50b919..7256c8194626 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 d496b3955de7..af0373451728 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 bbdd246835f9..37a61a9032ee 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 c8506926f516..093a73ae8644 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 cc2d5ab85251..d69252806ff7 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 2f5ece53d1bb..8530e4327092 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 1cf720092fb3..26f5fd1fdcb0 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 81f732f1b9ea..edaffaf49842 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 0dcb9da6a8c7..f18c5f0b44a2 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 f19a2bb328ef..b60412f67bfa 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 3a6e880bf6f2..3a3fa215ce4c 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 394eb5c3e077..346d8ded6ab5 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 ed20396bb382..e90182461b9e 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 134fa7771633..34a4e734ef80 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 1fb7e7df2461..900cdc1f330d 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 0c865e0ab8fc..4112fdb7cee2 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 05a3ae4728f7..44a0dcffa7be 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 e7597f7c313e..19e0d6ce66d0 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 04f6127e6fe0..14a9ebd96792 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 1ea151cf2317..5cc3dd142a7d 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 ca8c57bb3435..90f0911e24a0 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 57e7d44dab10..4339d423159f 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 32f11b08616c..218899f1f688 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 18b45155fdc6..bca91c2ed88f 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 51b4ff58e88f..475349e6d19a 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 e577289ed367..c4ee11c9b8d2 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 49e574c09fe7..decb69b6a702 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 1209bd4d351e..4b49d3d0079d 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 e9a08a3e196f..5c52335c8179 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 3adc95791f85..ddfe6145be59 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 b70000ed3f3b..0d143b8a6d10 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 b577c415fd24..03b26005459a 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 74f5306a0ba2..f4bc0aaf884c 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 7e5a3b2a44ed..cc287c3ca139 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 c581fd2757ad..fb54e0971a15 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 72797e4f057c..c195c9ddcd98 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/view/frontend/layout/catalog_product_view.xml b/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml index a6b46f8f25a7..aadcef81da88 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 8a853cdd2e40..815d7ee1f3ad 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 8a853cdd2e40..815d7ee1f3ad 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 6b00bf681c1e..17dbde65bf7e 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 5eb1f100c572..505dace8d214 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 42adc8009c01..f25e32575c75 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 8c0bca63f8ef..b177c915275a 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 936dbe973a3e..c2fe027715f6 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 b89f09f6afd1..f5c33c89af47 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/Model/Condition/Product/AbstractProduct.php b/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php index 10b32d393083..64e2e881409a 100644 --- a/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php +++ b/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php @@ -516,7 +516,7 @@ public function loadArray($arr) ) ? $this->_localeFormat->getNumber( $arr['is_value_parsed'] ) : false; - } elseif (!empty($arr['operator']) && $arr['operator'] == '()') { + } elseif (!empty($arr['operator']) && in_array($arr['operator'], ['()', '!()', true])) { if (isset($arr['value'])) { $arr['value'] = preg_replace('/\s*,\s*/', ',', $arr['value']); } diff --git a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php index cb4cb9a12bd5..ff676739695a 100644 --- a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php +++ b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php @@ -144,6 +144,7 @@ protected function _joinTablesToCollection( * @return string * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function _getMappedSqlCondition( AbstractCondition $condition, @@ -155,7 +156,7 @@ protected function _getMappedSqlCondition( // If rule hasn't valid argument - prevent incorrect rule behavior. if (empty($argument)) { return $this->_expressionFactory->create(['expression' => '1 = -1']); - } elseif (preg_match('/[^a-z0-9\-_\.\`]/i', $argument) > 0) { + } elseif (preg_match('/[^a-z0-9\-_\.\`]/i', $argument) > 0 && !$argument instanceof \Zend_Db_Expr) { throw new \Magento\Framework\Exception\LocalizedException(__('Invalid field')); } 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 e19d56dd46f5..b9312a8b407f 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 3c61384d8b84..c0963ba1e345 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 161b8405f11e..67a5c11f07cc 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 3449d0054b7e..981f793f3530 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 0c3b6ab5cb02..6190f06c10ed 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 3b3c8221596a..4761df08a73d 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 e55b5d60d1f6..d3042af0074d 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 e45405714956..aa9266e1d2d9 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 22f61d3583fa..49e9c71cb68b 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 474c148518f1..c50d4c7b9e9a 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 a32b2dbc74bd..853592a07f99 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 bfb668a67409..6d696818194f 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 8a7256d1f117..89a965dbb5de 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 57fc0441fe83..a08b8802db47 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 cb9c7315244a..cfa77c9b7348 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 689a55f06896..68f4a56a4195 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 7f93c94a9698..8fcf29b1a509 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 1cdb3989d8da..170b6970072c 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 3dcd30edd4f0..4e47343c3d99 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 12d2355cf3a1..7897b4e91bf3 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 65ccb43879ac..643ed5445231 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 c5832f64547c..2fda66e84c14 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 b0e860d7f2e2..9a8154f4c3c3 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 f7e7e2cc451f..29af1a8b9484 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 e344b258c519..c738e22dd616 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 a2aaed18cb56..15d853cabfbe 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 0e0d8213cb79..1e2e5dfb7966 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 5f6beead7a09..954458589a41 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 154ee6e845bc..f94be70782e7 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 585050c14cc1..d8c8105ffeb7 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 bae6223ee7d5..c2681b57df91 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 e4a0a0ba93e7..8f7b991f3ce4 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 f272a4638a17..af6d0af57fa9 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.php b/app/code/Magento/Sales/Model/Order/Address.php index f38345f59b8d..bb50a59eeea8 100644 --- a/app/code/Magento/Sales/Model/Order/Address.php +++ b/app/code/Magento/Sales/Model/Order/Address.php @@ -142,7 +142,7 @@ public function getName() { $name = ''; if ($this->getPrefix()) { - $name .= $this->getPrefix() . ' '; + $name .= __($this->getPrefix()) . ' '; } $name .= $this->getFirstname(); if ($this->getMiddlename()) { @@ -150,7 +150,7 @@ public function getName() } $name .= ' ' . $this->getLastname(); if ($this->getSuffix()) { - $name .= ' ' . $this->getSuffix(); + $name .= ' ' . __($this->getSuffix()); } return $name; } diff --git a/app/code/Magento/Sales/Model/Order/Address/Validator.php b/app/code/Magento/Sales/Model/Order/Address/Validator.php index 08872ca2925e..f35afb0100b6 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 aa33bb1a12ee..90756febb8a2 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 ddae9f5910c1..4d42d45bb7c2 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/Shipping.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Shipping.php index 0537e361c892..1bb796565ebe 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Shipping.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Shipping.php @@ -7,6 +7,7 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Tax\Model\Calculation as TaxCalculation; +use Magento\Sales\Model\Order; /** * Order creditmemo shipping total calculation model @@ -115,25 +116,26 @@ public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo) /** * Checks if shipping provided incl tax, tax applied after discount, and discount applied on shipping excl tax * - * @param \Magento\Sales\Model\Order $order + * @param Order $order * @return bool */ - private function isShippingIncludeTaxWithTaxAfterDiscountOnExcl($order): bool + private function isShippingIncludeTaxWithTaxAfterDiscount(Order $order): bool { - return $this->getTaxConfig()->getCalculationSequence($order->getStoreId()) - === TaxCalculation::CALC_TAX_AFTER_DISCOUNT_ON_EXCL && - $this->isSuppliedShippingAmountInclTax($order); + $calculationSequence = $this->getTaxConfig()->getCalculationSequence($order->getStoreId()); + return ($calculationSequence === TaxCalculation::CALC_TAX_AFTER_DISCOUNT_ON_EXCL + || $calculationSequence === TaxCalculation::CALC_TAX_AFTER_DISCOUNT_ON_INCL) + && $this->isSuppliedShippingAmountInclTax($order); } /** * Get allowed shipping amount to refund based on tax settings * - * @param \Magento\Sales\Model\Order $order + * @param Order $order * @return float */ - private function getAllowedAmountInclTax(\Magento\Sales\Model\Order $order): float + private function getAllowedAmountInclTax(Order $order): float { - if ($this->isShippingIncludeTaxWithTaxAfterDiscountOnExcl($order)) { + if ($this->isShippingIncludeTaxWithTaxAfterDiscount($order)) { $result = $order->getShippingInclTax(); foreach ($order->getCreditmemosCollection() as $creditmemo) { $result -= $creditmemo->getShippingInclTax(); @@ -155,7 +157,7 @@ private function getAllowedAmountInclTax(\Magento\Sales\Model\Order $order): flo private function getBaseAllowedAmountInclTax(\Magento\Sales\Model\Order $order): float { $result = $order->getBaseShippingInclTax(); - if ($this->isShippingIncludeTaxWithTaxAfterDiscountOnExcl($order)) { + if ($this->isShippingIncludeTaxWithTaxAfterDiscount($order)) { foreach ($order->getCreditmemosCollection() as $creditmemo) { $result -= $creditmemo->getBaseShippingInclTax(); } 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 04c698a8e1a2..e08ed2e2814a 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php @@ -5,13 +5,13 @@ */ namespace Magento\Sales\Model\Order\Creditmemo\Total; +use Magento\Framework\App\ObjectManager; use Magento\Sales\Api\Data\CreditmemoInterface; use Magento\Sales\Model\Order\Creditmemo; use Magento\Sales\Model\Order\Invoice; use Magento\Sales\Model\ResourceModel\Order\Invoice as ResourceInvoice; -use Magento\Tax\Model\Config as TaxConfig; use Magento\Tax\Model\Calculation as TaxCalculation; -use Magento\Framework\App\ObjectManager; +use Magento\Tax\Model\Config as TaxConfig; /** * Collects credit memo taxes. @@ -134,8 +134,8 @@ public function collect(Creditmemo $creditmemo) $baseShippingDiscountTaxCompensationAmount = 0; $shippingDelta = $baseOrderShippingAmount - $baseOrderShippingRefundedAmount; - if ($shippingDelta > $creditmemo->getBaseShippingAmount() || - $this->isShippingIncludeTaxWithTaxAfterDiscountOnExcl($order->getStoreId())) { + if ($orderShippingAmount > 0 && ($shippingDelta > $creditmemo->getBaseShippingAmount() || + $this->isShippingIncludeTaxWithTaxAfterDiscount($order->getStoreId()))) { $part = $creditmemo->getShippingAmount() / $orderShippingAmount; $basePart = $creditmemo->getBaseShippingAmount() / $baseOrderShippingAmount; $shippingTaxAmount = $order->getShippingTaxAmount() * $part; @@ -213,10 +213,12 @@ public function collect(Creditmemo $creditmemo) * @param int|null $storeId * @return bool */ - private function isShippingIncludeTaxWithTaxAfterDiscountOnExcl(?int $storeId): bool + private function isShippingIncludeTaxWithTaxAfterDiscount(?int $storeId): bool { - return $this->taxConfig->getCalculationSequence($storeId) === TaxCalculation::CALC_TAX_AFTER_DISCOUNT_ON_EXCL && - $this->taxConfig->displaySalesShippingInclTax($storeId); + $calculationSequence = $this->taxConfig->getCalculationSequence($storeId); + return ($calculationSequence === TaxCalculation::CALC_TAX_AFTER_DISCOUNT_ON_EXCL + || $calculationSequence === TaxCalculation::CALC_TAX_AFTER_DISCOUNT_ON_INCL) + && $this->taxConfig->displaySalesShippingInclTax($storeId); } /** diff --git a/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php b/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php index 331b1d760f9c..f5f591a9edab 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 000000000000..765a021eacaa --- /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 19dc62772513..22d5bc2b7a56 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 1ca2efef4d8f..585da4d96584 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 ba6d65a40c65..78aaf1a696e9 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 d7e373afce51..9b47bc1d465d 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 3a57ecf57893..b3b591acfd95 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 57d6c204dcaf..b5221b4f7c30 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 9b68fc2b6775..905f7f89697e 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 cc67601f0ec5..ce84dae87f69 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 48934e24a379..e9027744896b 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 6ddbce49829e..4560a65bf3c3 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 a88b508ba078..6b555c87cc66 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 ef9c6fc628dd..faff61d15fc3 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 0da936e74ca6..660b6acfba8f 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 c218b3ec93fc..1073ffd5b7e0 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 ad73b22e9455..cf72a0f1b93c 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 ccdca89efb65..6789dc646f48 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 6c5cfb6e3a3a..a9b0d74e6367 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 6ad8d73b1fc4..45946697ba89 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 83e7c9ada993..cbf281ab47d7 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 373d04ab9831..a7ca034ac057 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 190330846649..4f865ae5fc18 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 226b9cdf8a48..6564e35bfa1f 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 ce0245098031..30f8fecdb811 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 0ecc757ab761..9cd4cb2b313f 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 6ad8ebc3bb89..de3e88f950a8 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 da46e8fc068a..0c73958c2bbb 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 dcd7dbd9e637..acc86781c6ed 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 5ecbbd777a14..4a5c60c1d6f2 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 245299d1d067..64d51abb066a 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 e068da8fb08d..5647e1a32db3 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 65b11e1129b3..193c161da11f 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 e37e8ab843e7..91c3f2fd1cf2 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 12ff4adcc4d6..2234e8ed877d 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 995bb8335163..743bc8358829 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 69068f5bfc5d..9d4f6c369587 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 7dd4ef9c46cf..8e74f1c8b628 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/Fixture/InvoiceComment.php b/app/code/Magento/Sales/Test/Fixture/InvoiceComment.php new file mode 100644 index 000000000000..0ba09e4dd577 --- /dev/null +++ b/app/code/Magento/Sales/Test/Fixture/InvoiceComment.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Fixture; + +use Magento\Framework\DataObject; +use Magento\Sales\Api\Data\CommentInterface; +use Magento\Sales\Api\Data\EntityInterface; +use Magento\Sales\Api\Data\InvoiceCommentInterface; +use Magento\Sales\Api\InvoiceCommentRepositoryInterface; +use Magento\TestFramework\Fixture\Api\ServiceFactory; +use Magento\TestFramework\Fixture\RevertibleDataFixtureInterface; + +class InvoiceComment implements RevertibleDataFixtureInterface +{ + private const DEFAULT_DATA = [ + InvoiceCommentInterface::IS_CUSTOMER_NOTIFIED => 0, + InvoiceCommentInterface::PARENT_ID => 0, + CommentInterface::COMMENT => 'Test Comment', + CommentInterface::IS_VISIBLE_ON_FRONT => 0, + EntityInterface::ENTITY_ID => 0, + EntityInterface::CREATED_AT => "0000-00-00 00:00:00", + ]; + + /** + * @var ServiceFactory + */ + private $serviceFactory; + + /** + * @var InvoiceCommentRepositoryInterface + */ + private $invoiceCommentRepository; + + /** + * @param ServiceFactory $serviceFactory + * @param InvoiceCommentRepositoryInterface $invoiceCommentRepository + */ + public function __construct( + ServiceFactory $serviceFactory, + InvoiceCommentRepositoryInterface $invoiceCommentRepository + ) { + $this->serviceFactory = $serviceFactory; + $this->invoiceCommentRepository = $invoiceCommentRepository; + } + + public function apply(array $data = []): ?DataObject + { + $service = $this->serviceFactory->create(InvoiceCommentRepositoryInterface::class, 'save'); + $invoiceComment = $service->execute($this->prepareData($data)); + + return $this->invoiceCommentRepository->get($invoiceComment->getId()); + } + + public function revert(DataObject $data): void + { + $invoice = $this->invoiceCommentRepository->get($data->getId()); + $this->invoiceCommentRepository->delete($invoice); + } + + /** + * Prepare invoice data + * + * @param array $data + * @return array + */ + private function prepareData(array $data): array + { + $data['entity'] = array_merge(self::DEFAULT_DATA, $data); + + return $data; + } +} 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 000000000000..344855e01030 --- /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 5bf954e4c846..a074cfe9d5fc 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 000000000000..63082bc505ec --- /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 000000000000..438407e2e346 --- /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 ec4352c15e1a..65eaf37a5849 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 000000000000..90e52f56059c --- /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 2349be636cfa..2afa13f86000 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 7277a75c9bc7..71379830e7a4 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 a0d5dac5bb9a..83fb4e30d25d 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 220d47e0495f..4ce642096cff 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 883f1047feb7..a998f7e0b879 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 73a4da42eb09..9e396e98d4dc 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 000000000000..29acb66b12d8 --- /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 b41745596d05..69586c510685 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 8141d7fb534c..fb1da85bfb9e 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 bf194422defe..5dea2a716123 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 f05baf248ed6..3f0121880913 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 abeddac6d7f1..9350d3f3d94f 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 bcf8bdcae7c5..084da9633d00 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 9ce111663720..94441c5e7030 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 2e9b8b18d058..ab9911bce876 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 bd94e985fe3c..0497bdbeb26f 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 f17172a1f75c..499f067a3e1c 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 6af3c5bf4f58..2a12cb7593c4 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 4234d76e21ba..3c1d899454ba 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 58fe442cdee6..bc7c517f4c28 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 4ac48485127b..efa2cec38a96 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 340448eded2d..b5cd45010c9a 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 76b5e2ad81bd..dd4ba06ec7bd 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 1fef95650577..c4a9d66a9542 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 d569cb96707d..9320e62c8ce2 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 5964c656aa99..541bc41529e9 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 af98ac1a04d2..af97062d565b 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 a3c3ff90f39a..8f95e0f7252b 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 35b19b7b6922..6bbb4369b8d1 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 20ea41ef68fd..238e3416e32b 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 a89d29a54e31..ec4f61549de9 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 b2bfa93678df..0d700204658a 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 94091114baed..a906ccfa3a44 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 e6ed8f0240bf..b94f095db304 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 7c9a6593cf7f..c9f12d43001f 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 4d50217d3615..f52d8f2db016 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 c29b2aa167ed..28d2a5551e3f 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 141bf27c8f18..8a1fb8671b7c 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 ff5dc0e36fdb..8bdb8950602f 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 ee11a140500f..134af2548b35 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 2a30c814f6a1..27735d0f6644 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 36d319bf7112..cfc7a92e22b8 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 f122665c6fca..d0c5e94275cc 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 8b8789d488b9..804303ef42b2 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 219a3fcb4a04..f44e7bd3b06e 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 13b91fa605bc..cf17c1f54995 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 6570280b1118..51ead26cd6cf 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 6475688daa46..05121b1f8d3e 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 476eb161936f..a5bd0d98572f 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 903429e6a0b8..455ce9b1551a 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 5c49d29ddf22..748fb65680a8 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 5afe57614587..51015b8f0a73 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 000000000000..e6ac445c590a --- /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 4096a2473e97..cdfbf0b39ae3 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 649956ef8e1a..ba22cbbb2483 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 199fa01b2537..e5678f049c40 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 ac1d1dd841ef..97858b530c75 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 cb2d33420fb0..bf6933458d1c 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 6e8ec14fc67c..196246cdfb80 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 b19e1fc8eff9..7642b15c6348 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 a26b2dcd06ae..4097972cc9e9 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 000000000000..774e552d2228 --- /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 70a292f4ee26..3e6bf9ac36d3 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 3480fdc4dc9e..85299cef9a04 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 741928d3baa8..038d45c0ffd8 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 10b911e2d8f2..29fb7e9a8836 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 000000000000..8d0d6e457c86 --- /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 c9af87cdb115..36b2a51e9ec6 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 794d09226d87..1bd342fb973c 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 853fa5822f79..43859f68052e 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 bf90770ad849..d76ddd7a5bce 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 0abfcb3c8df6..b33597f9f588 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 592c8b7981be..0ca984f1da6b 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 9d1840d18a97..8536984210f8 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 23e71dcb03a0..4c1af9ec6f3a 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 49511a62e258..1b8aa5adf186 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 362b7c9794cc..24c06b6e2200 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 e8b842a48890..747a423be31f 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 38b85828c342..65812e8dbeef 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 000000000000..f987f8d5c78f --- /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 000000000000..962b99e6cd99 --- /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 fea3fe68fd52..6ac0a350a42c 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 2299a1254684..fc176167a6f9 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 e0576f94347c..201d429c61a1 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 9c3356760341..0e6a6b297c7a 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 dc96da653d6d..cba9df49d0ff 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 874164fdcdcf..ce09d34006f3 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 121b1a13333a..5a663edebc4d 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 772539dc63ba..6ade37148020 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 8e3a87aa8f67..6214078e2ee5 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 dce66e929b2d..ed5e2ae63c66 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 d5805176a064..6808f249480e 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 387e2b840384..a9d1e8716d60 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 deffdb63c463..a5bc987f03d1 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 f8136a9071a1..4e20350fff13 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 9ac7da076b10..81209408fada 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 1ad7b3db2127..5846565ff14a 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 acf04b273b2a..f1c6c681a6a2 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 a517d310f6a5..686ec150281e 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 7e18c3582011..7963e28e8b5a 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 814be5ccd86b..55da7963bad1 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 e66ed1d54544..bb575714ebaa 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 000000000000..79eeb1666de7 --- /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 b0c6b3a2fc6c..62d1311647ec 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 c6aa91facb38..4809de5ecf2f 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 88e3ada61068..c5baf949c79b 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 000000000000..8b7c2addb7b8 --- /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 af71fa8cf295..535ae0ab1628 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 4f20df24516e..ba4e469f95d3 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 457ee39f517b..d0ccae297f50 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 fd4dc8e4aa42..03b2a90a77e6 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 1ae0388b206b..7e759f02fece 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 f1f83dd8be3a..a2e14c6b1c23 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 31e833f0eab7..dbe3ac30152f 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 b5dfa255436a..a98baf207bf4 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 2d4dd3220f77..19c753157a19 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 87a6dbf8fdff..3e4437b02aec 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 004d2b72f8b3..7d3afedb4be1 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 c5a6545c3c84..be60eba36622 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 009037da2b50..5d2ac773ca53 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 000000000000..7f196fc015a1 --- /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 000000000000..9a2ca17a4162 --- /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 9ff5e61d74ac..d95d68e10fda 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 bf45d3305dcf..40d96b618ac1 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 8fd659a5bd6a..5b6b57694b22 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 ee7eaeb4fdcf..89ec56e516ec 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 1e97703acbe0..c5e6e601c973 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 cea6a37f6a57..5bd0ebd2c75d 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 d4d54b601318..45cedc24e043 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 75208d674558..2f57be188a24 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 54432b869933..918723ebff89 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 000000000000..2082d0db78e8 --- /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 474dc6f09ff0..f76f6cb942ea 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 83aee5ef3d89..afd06952d800 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 4d7c725ecab0..431d7d13a6a6 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 218dfeab8941..157c1982505e 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 6f35b062aad9..ce8dd62e6101 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 4ab85138f7c4..912716791879 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/Unit/Block/Reorder/SidebarTest.php b/app/code/Magento/Sales/Test/Unit/Block/Reorder/SidebarTest.php index e0b332444902..aa99fdcd3e48 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 2260b15616e5..8cdecc4e54f0 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 c870051b9355..93bdb1f18927 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 8ac618af1df5..422e8668aea1 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 6ea9f8c22111..d9304fa43ef6 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/ShippingTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/ShippingTest.php index 4ae0b3305e8e..998673a30e17 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/ShippingTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/ShippingTest.php @@ -509,14 +509,15 @@ public function testCollectRefundShippingAmountIncTax() /** * situation: The admin user specified the desired refund amount that has taxes and discount embedded within it * + * @dataProvider calculationSequenceDataProvider * @throws LocalizedException */ - public function testCollectUsingShippingInclTaxAndDiscountOnExclBeforeTax() + public function testCollectUsingShippingInclTaxAndDiscountBeforeTax(string $calculationSequence) { $this->taxConfig->expects($this->any())->method('displaySalesShippingInclTax')->willReturn(true); $this->taxConfig->expects($this->any()) ->method('getCalculationSequence') - ->willReturn(TaxCalculation::CALC_TAX_AFTER_DISCOUNT_ON_EXCL); + ->willReturn($calculationSequence); $orderShippingAmount = 14.55; $shippingTaxAmount = 0.45; @@ -603,4 +604,15 @@ public function testCollectUsingShippingInclTaxAndDiscountOnExclBeforeTax() ->willReturnSelf(); $this->shippingCollector->collect($this->creditmemoMock); } + + /** + * @return array + */ + public function calculationSequenceDataProvider(): array + { + return [ + 'inclTax' => [TaxCalculation::CALC_TAX_AFTER_DISCOUNT_ON_INCL], + 'exclTax' => [TaxCalculation::CALC_TAX_AFTER_DISCOUNT_ON_EXCL], + ]; + } } 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 ac88a01ce65a..91195ee675a7 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 67f1931cf7bd..000000000000 --- 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 946a053fb6ae..a7298f278f3e 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 000000000000..b5347c70cf36 --- /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 acecdb3e7626..49fe29436490 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 7b789dbfe471..3e8579a5a023 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 0ae4bddbe5ac..32ae440a2a4f 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/ShipmentFactoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentFactoryTest.php index e73838d85c7e..dd721da78ad7 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentFactoryTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentFactoryTest.php @@ -8,7 +8,6 @@ namespace Magento\Sales\Test\Unit\Model\Order; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Sales\Model\Convert\Order; use Magento\Sales\Model\Convert\OrderFactory; use Magento\Sales\Model\Order\Item; @@ -52,8 +51,6 @@ class ShipmentFactoryTest extends TestCase */ protected function setUp(): void { - $objectManager = new ObjectManager($this); - $this->converter = $this->createPartialMock( Order::class, ['toShipment', 'itemToShipmentItem'] @@ -69,12 +66,9 @@ protected function setUp(): void ['create'] ); - $this->subject = $objectManager->getObject( - ShipmentFactory::class, - [ - 'convertOrderFactory' => $convertOrderFactory, - 'trackFactory' => $this->trackFactory - ] + $this->subject = new ShipmentFactory( + $convertOrderFactory, + $this->trackFactory ); } 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 934bfa5f261a..44385f863374 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 966d16850f3c..d6579ae7ac36 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 000000000000..139b7c1c179b --- /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 28e4e763ed9a..09344ea068cc 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 112e927bf4c9..bdadd8df0b3c 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 02efd7d5a005..664c65d36c3c 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 d9ba0a654585..6b22ae756566 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 7e3dc7ea0be0..472edefda599 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 784d3f892f2c..7b2026c63872 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 a168a89ed5ef..cabad529c739 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 12b25abe1dec..9c46b12c0137 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 a1155dd436d4..07ba2ad31948 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 4fc110af0bae..44a129fe47c3 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 3bfcbb1426c2..1c2b8e50de2a 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 572a5e1313c4..2e3585dcf1b9 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 000000000000..ad2c3c8629d8 --- /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/Model/Resolver/Invoices.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Invoices.php index f106752075c2..02d0f7687894 100644 --- a/app/code/Magento/SalesGraphQl/Model/Resolver/Invoices.php +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Invoices.php @@ -41,10 +41,31 @@ public function resolve( $invoices[] = [ 'id' => base64_encode($invoice->getEntityId()), 'number' => $invoice['increment_id'], + 'comments' => $this->getInvoiceComments($invoice), 'model' => $invoice, 'order' => $orderModel ]; } return $invoices; } + + /** + * Get invoice comments in proper format + * + * @param InvoiceInterface $invoice + * @return array + */ + private function getInvoiceComments(InvoiceInterface $invoice): array + { + $comments = []; + foreach ($invoice->getComments() as $comment) { + if ($comment->getIsVisibleOnFront()) { + $comments[] = [ + 'timestamp' => $comment->getCreatedAt(), + 'message' => $comment->getComment() + ]; + } + } + return $comments; + } } diff --git a/app/code/Magento/SalesGraphQl/etc/graphql/di.xml b/app/code/Magento/SalesGraphQl/etc/graphql/di.xml index b40d8e9331bb..5fc18103f4b6 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 52229d892b98..41c6ad4e7e68 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 1a631886f1a9..b15c93743bb9 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 000000000000..98cf3e670665 --- /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 eeab18e9c360..a518a00c7352 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 d664cf30f570..2d4535120b1e 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 cfad4b5c09c5..ab5f1f86c8a1 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 19e9bdf377bf..c3ea7d26e9eb 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 42498448cd13..c22deb659b37 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 d65021ed82a2..125e2194b45e 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 c7a344230698..a2d4af289004 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 386642c22ab1..d35ed63e908f 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 2f9dbb9faea2..485b98c22565 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 a807bca77cc6..f40fe129a761 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 25f0ef91eae6..ea1376ad949b 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 ae2beb00d6fe..a77b372309d3 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 998d1c3a2a8e..abd200fe031a 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 02fd81078ea7..07e8aed8ef28 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 88fb4e2acd45..1f693e18c9ec 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 3efec5f652c9..bea6f4569d78 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 000000000000..014ed4e133dd --- /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 000000000000..98e763533c97 --- /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 000000000000..a8ca43ad6b6c --- /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 01fdc4dd120a..8b8491b3eb5e 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 000000000000..402d549b8c7b --- /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 5607512c862b..4a54472c248a 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 98b3c9b9ec96..c378c58008a2 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 bade99ad4717..80169374fd8d 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 60bf3d63e7e5..d097bb9eb8d5 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 a4318103c4c0..d88759e08ffa 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 18b9636e62e2..14fb4405c503 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 17420271d777..21fd57c6620d 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 c65aa9980666..59ee88b2feb8 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 25d9d431d1c5..428d149d01cf 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 65bb0b4cbfb9..805bb3e1a839 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 cd72ec852981..7168fa6a3ee0 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 fab4c79da628..a26002bbf9ed 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 83648cec149d..06c585250b23 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 6e1fcfc384f6..a58b3e8037c6 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 3aacc176acdc..3b96f89db7e1 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 8c02f401992e..279e8e5677a8 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 18183085060d..8ec440b494ee 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 d58912c58937..cf225add6e82 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 89e50d51d1ef..a06b9e66fab0 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 94c372c0eef7..6fc0c733cd53 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 2e31c7058f2b..ac54224095fb 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 2b2d74e2a01d..c749756770ec 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 9bcefdcfc314..bc070ff75cae 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 80eb79d9cc6f..f11542b92e5e 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 986aa6130eae..3f409cfa1012 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 a5221c3668db..93db67460886 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 76cc595d13c0..64cab0a6e676 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 190703b25a88..c6308721f183 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 2a0e6b60162a..dd0ec67e6a2b 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 5d51ee02ffe2..815484ee6b6d 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 3583cc7c1cf1..73ea0ebf1b0f 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 a8729ccd40f6..30227f610702 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 c039fcad311a..45a06bc59f10 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 d63df5fe50a6..86941d53fbb5 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 000000000000..beec1f98b870 --- /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 000000000000..0eb41babc0e9 --- /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 2a989f3d0e54..1a887542fc77 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/Unit/Model/Quote/Address/Total/ShippingDiscountTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/Address/Total/ShippingDiscountTest.php index 2ddf753b3c83..d7aa537e0314 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 5a7d6142a6d4..f9d14bec29e9 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 e452c8014518..119e5f7904ed 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 23a1df8777ab..25142edc8d45 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 af6f41cee229..d868bd30d2f0 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 82ca394effff..c72468b8351e 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 b080842df447..58e4cf42c02c 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 0fc7047c30b4..b00b3a74a648 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 0924cdcfe220..81f13fa1353b 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 6868617c1c44..99f9818c847c 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 443892b420de..7f3a9bd59fda 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 ab34b8a233e2..4cea80e4334d 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/Console/Command/SampleDataDeployCommand.php b/app/code/Magento/SampleData/Console/Command/SampleDataDeployCommand.php index 76159dc8320e..1358d80e8483 100644 --- a/app/code/Magento/SampleData/Console/Command/SampleDataDeployCommand.php +++ b/app/code/Magento/SampleData/Console/Command/SampleDataDeployCommand.php @@ -6,7 +6,6 @@ namespace Magento\SampleData\Console\Command; -use Composer\Console\Application; use Composer\Console\ApplicationFactory; use Exception; use Magento\Framework\App\Filesystem\DirectoryList; @@ -15,12 +14,10 @@ use Magento\Framework\Exception\InvalidArgumentException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; -use Magento\Framework\Serialize\Serializer\Json; use Magento\SampleData\Model\Dependency; use Magento\Setup\Model\PackagesAuth; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArrayInput; -use Symfony\Component\Console\Input\ArrayInputFactory; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -32,53 +29,31 @@ */ class SampleDataDeployCommand extends Command { - const OPTION_NO_UPDATE = 'no-update'; + public const OPTION_NO_UPDATE = 'no-update'; - /** - * @var Filesystem - */ - private $filesystem; + /** @var Filesystem */ + private Filesystem $filesystem; - /** - * @var Dependency - */ - private $sampleDataDependency; + /** @var Dependency */ + private Dependency $sampleDataDependency; - /** - * @var ArrayInputFactory - * @deprecated 100.1.0 - */ - private $arrayInputFactory; - - /** - * @var ApplicationFactory - */ - private $applicationFactory; - - /** - * @var Json - */ - private $serializer; + /** @var ApplicationFactory */ + private ApplicationFactory $applicationFactory; /** * @param Filesystem $filesystem * @param Dependency $sampleDataDependency - * @param ArrayInputFactory $arrayInputFactory * @param ApplicationFactory $applicationFactory - * @param Json $serializer */ public function __construct( Filesystem $filesystem, Dependency $sampleDataDependency, - ArrayInputFactory $arrayInputFactory, - ApplicationFactory $applicationFactory, - Json $serializer + ApplicationFactory $applicationFactory ) { $this->filesystem = $filesystem; $this->sampleDataDependency = $sampleDataDependency; - $this->arrayInputFactory = $arrayInputFactory; $this->applicationFactory = $applicationFactory; - $this->serializer = $serializer; + parent::__construct(); } @@ -107,35 +82,8 @@ protected function configure() * @throws FileSystemException * @throws LocalizedException */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { - $rootJson = $this->serializer->unserialize( - $this->filesystem->getDirectoryRead( - DirectoryList::ROOT - )->readFile("composer.json") - ); - if (!isset($rootJson['version'])) { - $magentoProductPackage = array_filter( - $rootJson['require'], - function ($package) { - return false !== strpos($package, 'magento/product-'); - }, - ARRAY_FILTER_USE_KEY - ); - $version = reset($magentoProductPackage); - $output->writeln( - '<info>' . - // @codingStandardsIgnoreLine - 'We don\'t recommend to remove the "version" field from your composer.json; see https://getcomposer.org/doc/02-libraries.md#library-versioning for more information.' . - '</info>' - ); - $restoreVersion = new ArrayInput([ - 'command' => 'config', - 'setting-key' => 'version', - 'setting-value' => [$version], - '--quiet' => 1 - ]); - } $this->updateMemoryLimit(); $this->createAuthFile(); $sampleDataPackages = $this->sampleDataDependency->getSampleDataPackages(); @@ -153,15 +101,8 @@ function ($package) { $arguments = array_merge(['command' => 'require'], $commonArgs); $commandInput = new ArrayInput($arguments); - /** @var Application $application */ $application = $this->applicationFactory->create(); $application->setAutoExit(false); - if (!empty($restoreVersion)) { - $result = $application->run($restoreVersion, clone $output); - if ($result === 0) { - $output->writeln('<info>The field "version" has been restored.</info>'); - } - } $result = $application->run($commandInput, $output); if ($result !== 0) { $output->writeln( @@ -173,12 +114,18 @@ function ($package) { return Cli::RETURN_FAILURE; } - return Cli::RETURN_SUCCESS; - } else { - $output->writeln('<info>' . 'There is no sample data for current set of modules.' . '</info>'); + $output->writeln( + '<info>' + . 'Sample data modules have been added via composer. Please run bin/magento setup:upgrade' + . '</info>' + ); - return Cli::RETURN_FAILURE; + return Cli::RETURN_SUCCESS; } + + $output->writeln('<info>' . 'There is no sample data for current set of modules.' . '</info>'); + + return Cli::RETURN_FAILURE; } /** @@ -189,7 +136,7 @@ function ($package) { * @return void * @throws LocalizedException */ - private function createAuthFile() + private function createAuthFile(): void { $directory = $this->filesystem->getDirectoryWrite(DirectoryList::COMPOSER_HOME); @@ -211,7 +158,7 @@ private function createAuthFile() * @throws InvalidArgumentException * @return void */ - private function updateMemoryLimit() + private function updateMemoryLimit(): void { if (function_exists('ini_set')) { // phpcs:ignore Magento2.Functions.DiscouragedFunction @@ -244,7 +191,7 @@ private function updateMemoryLimit() * @param string $value * @return int */ - private function getMemoryInBytes($value) + private function getMemoryInBytes(string $value): int { $unit = strtolower(substr($value, -1, 1)); $value = (int) $value; diff --git a/app/code/Magento/SampleData/README.md b/app/code/Magento/SampleData/README.md index e0666ba73fe2..569a5a3eb971 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/SampleData/Test/Unit/Console/Command/AbstractSampleDataCommandTest.php b/app/code/Magento/SampleData/Test/Unit/Console/Command/AbstractSampleDataCommandTest.php index 3bf664ea6b0d..dbce217bb237 100644 --- a/app/code/Magento/SampleData/Test/Unit/Console/Command/AbstractSampleDataCommandTest.php +++ b/app/code/Magento/SampleData/Test/Unit/Console/Command/AbstractSampleDataCommandTest.php @@ -26,16 +26,6 @@ */ abstract class AbstractSampleDataCommandTest extends TestCase { - /* - * Expected arguments for `composer config` to set missing field "version" - */ - private const STUB_EXPECTED_COMPOSER_CONFIG = [ - 'command' => 'config', - 'setting-key' => 'version', - 'setting-value' => ['0.0.1'], - '--quiet' => 1 - ]; - /** * @var ReadInterface|MockObject */ @@ -118,49 +108,21 @@ protected function setupMocks( ->willReturn($sampleDataPackages); $this->arrayInputFactoryMock->expects($this->never())->method('create'); - if (!array_key_exists('version', $composerJsonContent)) { - $this->applicationMock->expects($this->any()) - ->method('run') - ->withConsecutive( - [ - 'input' => new ArrayInput( - self::STUB_EXPECTED_COMPOSER_CONFIG + $this->applicationMock->expects($this->any()) + ->method('run') + ->with( + new ArrayInput( + array_merge( + $this->expectedComposerArgumentsSampleDataCommands( + $sampleDataPackages, + $pathToComposerJson ), - 'output' => $this->anything() - ], - [ - 'input' => new ArrayInput( - array_merge( - $this->expectedComposerArgumentsSampleDataCommands( - $sampleDataPackages, - $pathToComposerJson - ), - $additionalComposerArgs - ) - ), - 'output' => $this->anything() - ] - )->willReturnOnConsecutiveCalls( - $this->returnValue(0), - $this->returnValue($appRunResult) - ); - } else { - $this->applicationMock->expects($this->any()) - ->method('run') - ->with( - new ArrayInput( - array_merge( - $this->expectedComposerArgumentsSampleDataCommands( - $sampleDataPackages, - $pathToComposerJson - ), - $additionalComposerArgs - ) - ), - $this->anything() - ) - ->willReturn($appRunResult); - } + $additionalComposerArgs + ) + ), + $this->anything() + ) + ->willReturn($appRunResult); if (($appRunResult !== 0) && !empty($sampleDataPackages)) { $this->applicationMock->expects($this->any()) diff --git a/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php index a1186d601587..10efff4d04aa 100644 --- a/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php +++ b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php @@ -9,37 +9,12 @@ use Exception; use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\Serialize\Serializer\Json; use Magento\SampleData\Console\Command\SampleDataDeployCommand; use Magento\Setup\Model\PackagesAuth; -use PHPUnit\Framework\MockObject\MockObject; use Symfony\Component\Console\Tester\CommandTester; class SampleDataDeployCommandTest extends AbstractSampleDataCommandTest { - /** - * @var Json|MockObject - */ - private $serializerMock; - - protected function setUp(): void - { - parent::setUp(); - $this->serializerMock = $this->createMock(Json::class); - } - - /** - * Sets mock for unserialization composer content - * @param array $composerJsonContent - * @return void - */ - protected function setupMockForSerializer(array $composerJsonContent): void - { - $this->serializerMock->expects($this->any()) - ->method('unserialize') - ->will($this->returnValue($composerJsonContent)); - } - /** * Sets mocks for auth file * @@ -85,7 +60,6 @@ public function testExecute( $composerJsonContent ); $this->setupMocksForAuthFile($authExist); - $this->setupMockForSerializer($composerJsonContent); $commandTester = $this->createCommandTester(); $commandTester->execute([]); @@ -117,7 +91,6 @@ public function testExecuteWithNoUpdate( ['--no-update' => 1] ); $this->setupMocksForAuthFile($authExist); - $this->setupMockForSerializer($composerJsonContent); $commandInput = ['--no-update' => 1]; $commandTester = $this->createCommandTester(); @@ -139,7 +112,6 @@ public function processDataProvider(): array 'appRunResult' => 1, 'composerJsonContent' => [ 'require' => ["magento/product-community-edition" => "0.0.1"], - 'version' => '0.0.1' ], 'expectedMsg' => 'There is no sample data for current set of modules.' . PHP_EOL, 'authExist' => true, @@ -151,25 +123,11 @@ public function processDataProvider(): array 'appRunResult' => 1, 'composerJsonContent' => [ 'require' => ["magento/product-community-edition" => "0.0.1"], - 'version' => '0.0.1' ], 'expectedMsg' => 'There is an error during sample data deployment. Composer file will be reverted.' . PHP_EOL, 'authExist' => false, ], - 'Successful sample data installation without field "version"' => [ - 'sampleDataPackages' => [ - 'magento/module-cms-sample-data' => '1.0.0-beta', - ], - 'appRunResult' => 0, - 'composerJsonContent' => [ - 'require' => ["magento/product-community-edition" => "0.0.1"] - ], - // @codingStandardsIgnoreLine - 'expectedMsg' => 'We don\'t recommend to remove the "version" field from your composer.json; see https://getcomposer.org/doc/02-libraries.md#library-versioning for more information.' - . PHP_EOL . 'The field "version" has been restored.' . PHP_EOL, - 'authExist' => true, - ], 'Successful sample data installation' => [ 'sampleDataPackages' => [ 'magento/module-cms-sample-data' => '1.0.0-beta', @@ -177,9 +135,10 @@ public function processDataProvider(): array 'appRunResult' => 0, 'composerJsonContent' => [ 'require' => ["magento/product-community-edition" => "0.0.1"], - 'version' => '0.0.1' ], - 'expectedMsg' => '', + 'expectedMsg' => 'Sample data modules have been added via composer.' + . ' Please run bin/magento setup:upgrade' + . PHP_EOL, 'authExist' => true, ], ]; @@ -190,24 +149,11 @@ public function processDataProvider(): array */ public function testExecuteWithException(): void { - $this->expectException(\Exception::class); + $this->expectException(Exception::class); $this->expectExceptionMessage( 'Error in writing Auth file path/to/auth.json. Please check permissions for writing.' ); - $this->directoryReadMock->expects($this->once()) - ->method('readFile') - ->with('composer.json') - ->willReturn('{"require": {"magento/product-community-edition": "0.0.1"}, "version": "0.0.1"}'); - $this->serializerMock->expects($this->any()) - ->method('unserialize') - ->will($this->returnValue([ - 'require' => ["magento/product-community-edition" => "0.0.1"], - 'version' => '0.0.1' - ])); - $this->filesystemMock->expects($this->once()) - ->method('getDirectoryRead') - ->with(DirectoryList::ROOT) - ->willReturn($this->directoryReadMock); + $this->directoryWriteMock->expects($this->once()) ->method('isExist') ->with(PackagesAuth::PATH_TO_AUTH_FILE) @@ -237,16 +183,14 @@ private function createCommandTester(): CommandTester new SampleDataDeployCommand( $this->filesystemMock, $this->sampleDataDependencyMock, - $this->arrayInputFactoryMock, - $this->applicationFactoryMock, - $this->serializerMock + $this->applicationFactoryMock ) ); } /** - * @param $sampleDataPackages - * @param $pathToComposerJson + * @param array $sampleDataPackages + * @param string $pathToComposerJson * @return array */ protected function expectedComposerArgumentsSampleDataCommands( @@ -267,7 +211,7 @@ protected function expectedComposerArgumentsSampleDataCommands( */ private function packageVersionStrings(array $sampleDataPackages): array { - array_walk($sampleDataPackages, function (&$v, $k) { + array_walk($sampleDataPackages, static function (&$v, $k) { $v = "$k:$v"; }); diff --git a/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataRemoveCommandTest.php b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataRemoveCommandTest.php index 9883100ce5c4..281a66ce16c3 100644 --- a/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataRemoveCommandTest.php +++ b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataRemoveCommandTest.php @@ -89,7 +89,6 @@ public function processDataProvider(): array "require" => [ "magento/product-community-edition" => "0.0.1", ], - "version" => "0.0.1" ], 'expectedMsg' => 'There is an error during remove sample data.' . PHP_EOL, ], @@ -103,7 +102,6 @@ public function processDataProvider(): array "magento/product-community-edition" => "0.0.1", "magento/module-cms-sample-data" => "1.0.0-beta", ], - "version" => "0.0.1" ], 'expectedMsg' => '', ], diff --git a/app/code/Magento/Search/Test/Mftf/Test/AdminGlobalSearchOnProductPageTest.xml b/app/code/Magento/Search/Test/Mftf/Test/AdminGlobalSearchOnProductPageTest.xml index c1c9636ca149..4ca8e4060f8d 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 88e459178edb..55c58b0d27c5 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 000000000000..02c56487828d --- /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 090fc1d3cb50..5f02ede419d3 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 000000000000..dde01a641bfa --- /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 000000000000..9cd8c264e062 --- /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 556765cd69a7..adbf08797dba 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 14be6c7c66aa..c401d776f9d2 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 be22ed0872bd..2fb1f2b424b6 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 6dc07a6ea868..e7e10fd730e3 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 42d402a8ace8..d841797c34e6 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 4f8cd9da856c..9638d187c9f1 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 000000000000..211fdcd0641c --- /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/Security/README.md b/app/code/Magento/Security/README.md index 76ece8057edc..2522ef72d543 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 f901acb8cae6..e92c9e5adc96 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 844fc0a41c7b..a16d708eff73 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 51c92e21e476..5d83e2a27947 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 90c0864c29aa..09b0d4efb797 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 00c123aebdc0..e00a8b6c976c 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 ad1118fd725d..f65d5887111b 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 70d08e3622f9..821844a31c6e 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 d7151aff22fa..cfd7e4d60738 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 07f8ab82822a..2222bbf33407 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 7f6e57322fa3..08169881342f 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 37ad7e0048f3..659a096d75c5 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 12757ffea863..85d5aa6becbb 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 1ffd970bc14d..4d29fae7243f 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 ff2806db473f..3c214b620b65 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 6e866893fa51..3fdb41ff7de8 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/Security/etc/config.xml b/app/code/Magento/Security/etc/config.xml index 89a9cc9a1464..0e6b97f1203a 100644 --- a/app/code/Magento/Security/etc/config.xml +++ b/app/code/Magento/Security/etc/config.xml @@ -18,7 +18,7 @@ </admin> <system> <security> - <max_session_size_admin>256000</max_session_size_admin> + <max_session_size_admin>1024000</max_session_size_admin> <max_session_size_storefront>256000</max_session_size_storefront> </security> </system> diff --git a/app/code/Magento/SendFriend/etc/adminhtml/system.xml b/app/code/Magento/SendFriend/etc/adminhtml/system.xml index 0092fe4ab291..8102301feeb4 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 96a0665df4d3..d8e4b1772e7e 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 4d6f3d8c628b..0f76607a4ab7 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 bcfc243a4364..2e3058cae896 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 d903a1a7d588..8021bf0f93ce 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 d8a16023702d..9d189bd6f86d 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 000000000000..00cf51dbf87b --- /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 c86b6ccefa3f..b41d1f709e7b 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 0c0372850a3c..c776fb6fca27 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 000000000000..6b1660ba6698 --- /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 a043d9c83043..85b423701d7c 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 47727f19bef0..77f6484a301a 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 acbe29dd14e7..d658c1c99380 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 b1fb2aad5427..ee74ec419ce0 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 5d46ef0a7626..7f5595af7188 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 f99a4808ba8c..d80fede6a1eb 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 71f5ca84d342..6d0abc5b7dad 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 d60dca08e681..7d8492301c3e 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 6a2d1d6c33a6..39a09887ef52 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 52fefbe2cf75..ee71c93d48b5 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 000000000000..f97dc3f30ab7 --- /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 000000000000..2e28662f95f7 --- /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 53e91fbdb24c..3d11f0611d7e 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 c17451737577..e2f844f1c8a1 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 0f2f7ed26f1e..7ecd41fdbe32 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/Unit/Controller/Adminhtml/Order/Shipment/ViewTest.php b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/ViewTest.php index aa983aa5c86c..8cea497dd62a 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 29862bdcfc8b..a6611a2792b8 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 9caa2d5133d5..60f7c92782d8 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 d023f614f55a..505386e588f3 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 560797cfc745..a29c5413335c 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 1543fc8df933..a24e507cab35 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 92cbcbd500e8..50c173ab97e8 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 1bca90e32eaa..fe2d0bb17d89 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 4baa3c2e7d54..a9b0ef9a8ca9 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 c61e08e25593..f992724d3435 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 1ac6227dfc4d..5ccdfc683e0b 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 4125f3c82e6e..1220495021ab 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 000000000000..9bcc3c0ffad7 --- /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 af14fde52c13..6e20f5063f89 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 58340c6cc35a..000000000000 --- 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 d4635b46f02b..73a72e392b1d 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 000000000000..61c21d408702 --- /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 000000000000..149830821353 --- /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 000000000000..ef7342eb61c0 --- /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 e7e763aa28e8..3b6ad503cf5a 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 537802d312ee..ae1a53d2d2b7 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 7f1e71c42225..ac0409c70cb4 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 97df81ff6ab7..766f625df9db 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 c3137150c808..ae43f1830d89 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 1fc96a112894..afac427c49d9 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 877dd4a3cab2..f56b8c6bcdc3 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 bf6fb1e1b7a1..9a538eef61f7 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 000000000000..75f9787ac5ac --- /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 14160835af3e..afba374efe11 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 c0927884bdd2..7376976b12a5 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 781cd680a6c3..51d35f318ebf 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 56c7c3613ad9..d9040ff6c7ea 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 61b410707046..e98ed2c23829 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 ca8121ac3770..4f33cbffd19e 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 affb30d89076..a0b6607e6fea 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 febc5396c6bc..773b9da016e6 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 794a55929932..5b352da19bbb 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 33e1a0ffedee..8925769271b3 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 15ff6c4ca0f7..aa013d30903e 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 f2bfc7f7cea7..e65256a373a2 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 db36386101ab..150f56c8d767 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 c80541365768..4126a753ae6e 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 0e6f62ef93e6..081147284c8f 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 5199b27f1fe5..bb3ec1949017 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 3abdd1a7e66c..ebbe91205f45 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 7069e692d250..55534f6f9605 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 e72ef56b899a..8e03ebfaa90d 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 c24f82c09bef..da5ec87af08a 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 9826a5a1e0bc..b2b7cab8e47c 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 6c3b9f8fd689..430aa0a9bfcf 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 d56d88b16863..5557dfdd9b4e 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 854c1025de5e..8a11aa5106aa 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/Unit/Model/App/EmulationTest.php b/app/code/Magento/Store/Test/Unit/Model/App/EmulationTest.php index 43be65bf9005..f56ceb326b82 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 000000000000..513f674323ca --- /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 4d95135a07d9..bb924bd060ee 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 59c967e79dd3..986c655b18ce 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 178251e85084..5e9c2c63637e 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 bfca3e7341ef..0de02564c83a 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 e6e21f6ec0ae..26fcbad0ff1b 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 984a16eb3496..8ec1c8e6f1b5 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 000000000000..505d1965c450 --- /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 000000000000..c6e96c90e0f7 --- /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 000000000000..1a8fb6abc681 --- /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 000000000000..cee8556cdb20 --- /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 000000000000..a8342d92c49a --- /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 000000000000..640ee20a71af --- /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 000000000000..d400a378d43f --- /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 000000000000..2361f277a45e --- /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 f5fd98fdc4ca..c51fa91f121e 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 000000000000..2405641e9476 --- /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 bbec6a85a1a1..5d41698adb05 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 2a6c030aacb7..4ee605a01fcd 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 b63efe9a4dbd..c5cb15bddfcf 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 3529848949d7..7efa4089a4f0 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 373733639c65..3eeb7a1566c9 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 dd257de331b9..ede204a5e21b 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 801a8f32f3fc..507ce9a8a02b 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 000000000000..97d905e0b3b4 --- /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 000000000000..3533d8c34396 --- /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 000000000000..b35b1d68b1b1 --- /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 449f917463fc..1c45331b83f8 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 43746fc08a0d..4bec27a8d4ad 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 91975e449ff9..f5a2b47dd926 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 000000000000..cee3b0688f27 --- /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 0f65cf98b8ab..86350f44df14 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 14dab5dbb2c8..936e15eacc3e 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 150c0cf13d01..440d6d19d43a 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 07ce30b702f9..639919873649 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 000000000000..aa0a01269e35 --- /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 000000000000..3f44720bf293 --- /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 e5f9b70f1af6..14847361f9eb 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 8e3883a87111..6e61db4eae59 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 000000000000..eb08b639dce8 --- /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 02d08f52d901..8f8ad9fe1b6a 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 e05496efaa15..d5bbfb8ed0e9 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 8ecae7e0137a..501f1c3ea677 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 0d28be1b9463..f32eb128544f 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 262d9fd7c4c4..c58ae1b0fc0c 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 3288abbbb8d2..995b933e43d9 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 450d56ea28e0..c110052df43b 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 0add6159d5e4..9ceedfb0392a 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 c87dbc638270..525abfa7f901 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 000000000000..3afa2e8644eb --- /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/Unit/Block/Product/Renderer/Listing/ConfigurableTest.php b/app/code/Magento/Swatches/Test/Unit/Block/Product/Renderer/Listing/ConfigurableTest.php index 9e7e62e0a077..7f58641d4d22 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 740eb5e07b99..734cde2b4cc3 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 e3157b934b6a..b4ec69801ab7 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 7199bfa62863..fb21b4dc10de 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 f841f9c047b8..fcb610fcb58e 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 c0f5ccd95ba9..2624946e904e 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 5e045d94de45..0590ac6afa5b 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 7ec16fd7f537..7db649ae894e 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 33ad7539be4b..a798f2e03d51 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 9612b57f8d5d..2c1434b1cb68 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 36e90804b137..5b1d0961a6d5 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 16d828542c5b..3b042d40e4f7 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 8ba846dc710b..37785078e026 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 fefb90bf11e2..5413a21f5d66 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 b3add8ffab55..da7d4e1e1c80 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 8bb652107814..6db6b0300278 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 b184004b99dd..f0663674108d 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 030b2974ce97..6ccf10e6d181 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 b99f2776adf6..c8c57b70586f 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 164509fbc7f8..e92a15c471a8 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 668d104f3ccf..a3fa80db83f1 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 646da1441f22..3955ae943340 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 00de17ff5d3b..036d8533a585 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 1dd699cca311..4fb6ef5c6b30 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 2ae4165a82e8..47775b7f58ab 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 535336a513c6..f287602c77c3 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 32cc3ae7f491..27dfa65e251d 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 91fd0f4dcffb..1998f02f0909 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 2d355b6cc48c..b5dc49e48457 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 68e35e14bc47..bb946c4d9274 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 60cb6fe2898a..0079f0b93d68 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 65b2494fb847..1a865f884da5 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 8fad3427a3c9..84b9ee546bb0 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 71147e29ef59..d6a27188de27 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 a65598f5b491..e5508aa99b60 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 653f8c473e55..f6b762d0fc94 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 de65b778a3b0..9d379ea3cb98 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 5fc2d5e4dc54..318e35792e51 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 6897a7d9e75e..920bf959c1c2 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 ac18dfec6c7e..a71f885418e8 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 bad9757dafd8..2a354ea4376d 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 f449b1c6c6cb..5f3193d43821 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 97d59a51bb68..5424d6549ac6 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 000000000000..73bfd9b58c0f --- /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 000000000000..e2f966cd1086 --- /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 000000000000..dcf3d3bf8ecd --- /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 000000000000..07ced4494bdf --- /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 4b8d79117eb2..f7431b05d513 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 bc6099790431..4bc1c068570f 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 9df812586192..2ada8547f78d 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 da6528215887..f55edbf36162 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 000000000000..1e6bb20ad7a8 --- /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 9b67208f7749..b666e9fc5359 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 000000000000..9a080afe0573 --- /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 b2fd51225eaa..9b991bc7f409 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 2f418dddf388..1bc04926ae2f 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 0ede3caacd86..7b8816cbc0b3 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 cb597273e36b..39cb03a8a598 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 46d3582681c5..bb2d59d11cab 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 f428aabddcf9..1c8ffa4d8497 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 0e541b893905..aa87bfd729cc 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 7b9712fc30a4..3a791336c561 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 21f8b844adb5..84d1f1b162b0 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 25b919722ced..ca70751dfe06 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 7ba6caf5402b..f198197ecdc6 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 2fde2e2cd02d..74962cc92eb5 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 5cc17527c980..8800713d9ae8 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 a091fa5c9960..4189c52d9dbe 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 49c686618245..a6037102a7b2 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 8cd85ee0ca96..ce5cd6d571f3 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 addd8d283241..bf1a72a626e0 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 000000000000..d419b58bef19 --- /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 65945f80048a..0715af73e1bf 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 c208912654fd..c760cecf4d59 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 711307b6579c..2192d55755aa 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 ec40cd835d38..6cab1fc9b358 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 5f288d55b5d0..04493111d004 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 c5749a3a091d..5a5ae1e153da 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 484135f96735..3ef5faae1fc5 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 25cebd883f0b..63b757f3bb69 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 e3fef8091cf3..6a4c2931da71 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 126534ada9bd..5cc2c12bb8e7 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 04b1ca9f2296..7f6ac77aff57 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 e8daba77c926..caccb8756243 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 52acb40a5b02..43b3b3489d69 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 7ced4d382135..36478b61b9a7 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 220a5049932d..d293e6431921 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 6f7ef59788f6..58ef84485e96 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 c7663acf97a1..227329ceda05 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 5776925354e8..1d3923d954dd 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 c4449e5d6e5a..e414b04ea6fc 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 2bac4ca2115c..328cb58d5c39 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 c808de2d7f10..16ac05506c05 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/Unit/Model/Calculation/RowBaseCalculatorTest.php b/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseCalculatorTest.php index 552e94ddb783..82f18e0480af 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 8b334fb6e9ec..9639b8ceab3f 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 b0677bce00c8..25f5400cd2b9 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 8c97b28f9c9d..31490347b259 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 b1b1deb64d52..668ba9c0c3b6 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 ac13f8a5e8fe..a48325bee137 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 2d45da37d010..2fe634b1ff84 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 fa2c7d08e704..867286340643 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 0141101ef5a7..26573e358ce8 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 6813f780776e..830342ab9884 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 b83fe02f897a..e17c2d86d7a5 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 075b7a5d0662..0ac035725179 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 7f9b9cf86a80..672e176b5da8 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 e93a5a8b925a..1550ebaa367d 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/Block/Html/Topmenu.php b/app/code/Magento/Theme/Block/Html/Topmenu.php index 4521e38532ab..f8460b43ba2f 100644 --- a/app/code/Magento/Theme/Block/Html/Topmenu.php +++ b/app/code/Magento/Theme/Block/Html/Topmenu.php @@ -356,17 +356,6 @@ public function getIdentities() return $this->identities; } - /** - * Get tags array for saving cache - * - * @return array - * @since 100.1.0 - */ - protected function getCacheTags() - { - return array_merge(parent::getCacheTags(), $this->getIdentities()); - } - /** * Get menu object. * diff --git a/app/code/Magento/Theme/Model/Config/Processor/DesignTheme.php b/app/code/Magento/Theme/Model/Config/Processor/DesignTheme.php index d9d2c0e041e9..f1feac56cfba 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 04e4c131dbcd..c1a6bf810edb 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 000000000000..f2f5f509d8b6 --- /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 9035df639526..4bd55394389a 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 056b4c3f914f..ddf930b0ea80 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 5cfc06664b39..a96c79d9f69a 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 167191ee69a7..7be106ea58e7 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 8b49375bacd1..801c198c3d3f 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 69673fa5e6da..f0725bb8b164 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 76cfaa461dfa..8d35de6c135e 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 0f9ff05af3d8..56947a6d713f 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/Unit/Block/Html/FooterTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/FooterTest.php index 7682c83e0d38..8a8cbbe8b458 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 f9055f98d777..b17826210427 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 81324104cc3e..0df103fe1282 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 574e553b2b1f..c77972a009e8 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 6ea495e2702a..69fd87ab0eb7 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 0ef598c79259..6e797b1bfff5 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 9cb89746ad85..c3dd9e7af77d 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 dfe11f3120cd..e308caf83587 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/adminhtml/web/favicon.ico b/app/code/Magento/Theme/view/adminhtml/web/favicon.ico index d467f2bcbaed..cf0e2921e5cd 100644 Binary files a/app/code/Magento/Theme/view/adminhtml/web/favicon.ico and b/app/code/Magento/Theme/view/adminhtml/web/favicon.ico differ 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 2adbb28b9c59..23b2080ded62 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 d7fbc2979ea4..084dba302478 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 d2803a741d9a..58ae03b4e527 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 f863da70e898..1ef50f0cacfe 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/favicon.ico b/app/code/Magento/Theme/view/frontend/web/favicon.ico index d467f2bcbaed..cf0e2921e5cd 100644 Binary files a/app/code/Magento/Theme/view/frontend/web/favicon.ico and b/app/code/Magento/Theme/view/frontend/web/favicon.ico differ 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 388166b2b166..5e574e342114 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/Theme/view/install/web/favicon.ico b/app/code/Magento/Theme/view/install/web/favicon.ico index d467f2bcbaed..cf0e2921e5cd 100644 Binary files a/app/code/Magento/Theme/view/install/web/favicon.ico and b/app/code/Magento/Theme/view/install/web/favicon.ico differ 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 000000000000..a619756895e9 --- /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 4a3ca10f56f8..2b329238ef39 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 b24917c7fe81..d23778b81d78 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 1c95428ea93e..d668256847f5 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 44aacd0cfa44..f745a85895a3 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 b7dd1a858e4a..cc3105258bbd 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 8c10f7a3dae8..0b71aaf85e30 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 30edbe4aade1..36bfc0aa8f1b 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 f7ce9d1b4bb0..f00a6e5ea004 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 fe4069f0f28e..6cbec8499e20 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 d6141402f180..8a37a2fee069 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 5fd69cd6850d..5544ebf518ac 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 a5818f45b82a..95ed90b1dc24 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 298ae22cb890..40d42d834b00 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 09f5674b7adb..3c3c002603f2 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 e7dc245d47d6..988ff2ffe76f 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 d446ad8f7838..4e85a4a84bc5 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 ca3a78a7318b..365d60001110 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 56d524290280..c8d11b6cdf37 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 3047d1afcafb..99d7e9cc3fc4 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 000000000000..9677cebf5375 --- /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 624e82656aa8..c1c184d76803 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 296b26ea9970..57b87489e9bc 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 49100d466393..67351c9a767d 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 b34eea5aa226..bc76f7e95af1 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 7d098137ec53..a48a06578484 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 9208022be43e..da2120cf55ed 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 05e6761e17ce..000000000000 --- 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 000000000000..5337d92bf9fb --- /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 f330695867e7..5fe832a64e66 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 f339d0a5b702..6871ee81ba16 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 58ac4ef53861..000000000000 --- 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 edf1a3c9243a..e0bc9a160a05 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 000000000000..371c1aa4d194 --- /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 658bf756aacf..000000000000 --- 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 000000000000..a2f492521d20 --- /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 88fe2de81a3d..000000000000 --- 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 000000000000..5c24f32ade2d --- /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 1732594c57ea..000000000000 --- 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 000000000000..dac4a95f4521 --- /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 8de6b4598276..000000000000 --- 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 7b8b3a906781..000000000000 --- 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 97a19e5086d7..000000000000 --- 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 e84e3aa7aefb..000000000000 --- 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 b5711f9f12bf..000000000000 --- 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 6890e1bdaf87..269d33b1ad9a 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 73b10dd5ff41..52290a2bea82 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 08d751fc3e2c..5e0febf34c24 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 b6b7040a41bc..a1f12175c6b5 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 000000000000..5c334d9b13f7 --- /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 000000000000..c6737f5ec801 --- /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 6fb587fae736..93e6caebe2ba 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 40280f5c7b2c..794eabfe299d 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 ee5e186ee56c..13382a075ebd 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 226ca283c9dc..0e453e13455f 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 93a6f1cb5098..e1b0a4af481f 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 884e2e87a9c5..f47efa6d2ecb 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 feb6dd23e063..ceb189a7c3b7 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 ec5b3e92f828..65589be0a137 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 66e39a7860f3..22a1f55b8265 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 14ecc1511bd4..d1ca4595a7ab 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 2f64d0898c30..b1978964d0d4 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 69dc78b9d08d..ad0e0049f188 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 bf106f1eb9ee..98401fcd65d8 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 d6daad250a96..9a7b33eb94ed 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 87f8510a78a1..f172e32c60c6 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 d65fb96a7f5b..cb8fa64bd9ca 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 78691d8cbd88..41e58cf59c59 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 c40a2b9426fe..6312ad2513e0 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 121b04cc8ed1..3bfa398b5117 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 000000000000..a5f81cd4b2d2 --- /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 b7ddd085f932..016d20051399 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 af5aba50f654..2d8a2310eb48 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 8575f1d33c43..2bcee4e16851 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 c1083b866eae..71f24f30636a 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/InvoiceTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/InvoiceTest.php index 8b18d4bd07d1..5d4495e9a5c3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/InvoiceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/InvoiceTest.php @@ -7,9 +7,23 @@ namespace Magento\GraphQl\Sales; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Checkout\Test\Fixture\PlaceOrder as PlaceOrderFixture; +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\Customer\Test\Fixture\Customer; use Magento\Framework\Registry; +use Magento\Quote\Test\Fixture\AddProductToCart as AddProductToCartFixture; +use Magento\Quote\Test\Fixture\CustomerCart; +use Magento\Quote\Test\Fixture\GuestCart as GuestCartFixture; use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\ResourceModel\Order\Collection; +use Magento\Sales\Test\Fixture\Invoice as InvoiceFixture; +use Magento\Sales\Test\Fixture\InvoiceComment as InvoiceCommentFixture ; +use Magento\TestFramework\Fixture\DataFixture; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; use Magento\GraphQl\GetCustomerAuthenticationHeader; @@ -410,6 +424,64 @@ public function testPartialInvoiceForCustomerWithTaxesAndDiscounts() $this->deleteOrder(); } + #[ + DataFixture(Customer::class, ['email' => 'customer@search.example.com'], as: 'customer'), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture(CustomerCart::class, ['customer_id' => '$customer.id$'], 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$']), + DataFixture(PlaceOrderFixture::class, ['cart_id' => '$cart.id$'], 'order'), + DataFixture(InvoiceFixture::class, ['order_id' => '$order.id$'], 'invoice'), + DataFixture(InvoiceCommentFixture::class, [ + 'parent_id' => '$invoice.id$', + 'comment' => 'visible_comment', + 'is_visible_on_front' => 1, + ]), + DataFixture(InvoiceCommentFixture::class, [ + 'parent_id' => '$invoice.id$', + 'comment' => 'non_visible_comment', + 'is_visible_on_front' => 0, + ]), + ] + public function testInvoiceCommentsQuery() + { + $query = + <<graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + + $invoice = $response['customer']['orders']['items'][0]['invoices'][0]; + $this->assertCount(1, $invoice['comments']); + $this->assertEquals('visible_comment', $invoice['comments'][0]['message']); + $this->assertNotEmpty($invoice['comments'][0]['timestamp']); + } + /** * Prepare invoice for the order * 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 b3b4b9331d21..f28399fbcde4 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 299bccc5a127..b140aab0734f 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 013d8d5e4000..000000000000 --- 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 000000000000..d414a464daf5 --- /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 000000000000..fd30d65db187 --- /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 000000000000..feafaa4f7a05 --- /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 c79bbf0e0a30..80f55d83a40e 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 000000000000..357aba87db9b --- /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 df865286a91e..000000000000 --- 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 e4109cc8f779..4b35f8eb7a63 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 ce9e4ee94178..68cc2c2b2315 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 ad7c8ad25dbf..000000000000 --- 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 173e817c94c4..41b03933f40a 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 08ba18552c81..a7247e72ebad 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 000000000000..30c77cc2eaa0 --- /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/TestModuleCatalogSearch/Model/SearchEngineVersionReader.php b/dev/tests/integration/_files/Magento/TestModuleCatalogSearch/Model/SearchEngineVersionReader.php index 3c49b2ed63ff..b565caae4e3f 100644 --- a/dev/tests/integration/_files/Magento/TestModuleCatalogSearch/Model/SearchEngineVersionReader.php +++ b/dev/tests/integration/_files/Magento/TestModuleCatalogSearch/Model/SearchEngineVersionReader.php @@ -31,6 +31,9 @@ class SearchEngineVersionReader public function getFullVersion(): string { $version = $this->getVersion(); + if (strtolower($this->getDistribution()) == 'opensearch') { + $version = 1; + } return $this->getDistribution() . ($version === 1 ? '' : $version); } 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 000000000000..7bb3564b5185 --- /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 000000000000..5101e71cf9ee --- /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 000000000000..ac0313adf9f2 --- /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 000000000000..87ef08c0e281 --- /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 000000000000..fa484c0a9f85 --- /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 000000000000..997d43ff32a2 --- /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 000000000000..cccc747dc6ba --- /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 000000000000..0dd27bb7f9dd --- /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 000000000000..41195dbc025f --- /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 000000000000..4e286010bbeb --- /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 000000000000..edffb1f4a353 --- /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 000000000000..28100445340e --- /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 000000000000..660fb27e91f1 --- /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 000000000000..befec29700a9 --- /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 000000000000..d2db29fc6e5f --- /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 000000000000..8b4a77751313 --- /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 000000000000..265ea00cac21 --- /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 000000000000..0695a5db7428 --- /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 000000000000..7c69142380b7 --- /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 000000000000..b312fbfa758e --- /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 9f22458b9405..bc9cede4e112 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 0c6c546149f7..e878c2e680bd 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 7fe25f3a6f61..3ef5fe7d32be 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 321018055328..be4ccfde6266 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 2add2ed48fb9..419cbde64d9b 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 7c076452ea7f..eab1cb8646a0 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 000000000000..e3ea142d47dc --- /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 fe3f57ab9cd8..ca8e60e18a34 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 dc99055f87c7..8e41390286e7 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 07008d79218c..b13ad62c95a6 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 298554130df3..1681e14e9385 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 acfbadeaf2d9..6e4242ae6794 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 8941ae0ab7cb..95ddcdb05ddc 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 faac1b61ae51..000000000000 --- 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 a814a7faea34..cd7d35ae53b0 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 091bf25f2426..ef81c4509765 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 852308e83c27..3d5d98036413 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 576927184ba8..e62e4ed8247e 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 000000000000..857a9ceb1c24 --- /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 6d3761fdfcb7..582859b3494b 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 7af3527517d9..91a41fc27a60 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 517109625424..e6f351403a77 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 2ae79f07cde6..f7000c45c3cb 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 df8d79c5fff6..0c8ab6ae8de1 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 d3857b2fc0d6..b97f8ab391e6 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 000000000000..dfdadd17f60e --- /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 864bdaa2a133..c87473d369f9 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/BundleImportExport/Model/Import/Product/Type/BundleTest.php b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/Product/Type/BundleTest.php index e8c0a63a8968..c33636d6790a 100644 --- a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/Product/Type/BundleTest.php +++ b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/Product/Type/BundleTest.php @@ -39,11 +39,6 @@ class BundleTest extends \Magento\TestFramework\Indexer\TestCase */ private const TEST_PRODUCT_TYPE = 'bundle'; - /** - * @var \Magento\CatalogImportExport\Model\Import\Product - */ - protected $model; - /** * @var \Magento\Framework\ObjectManagerInterface */ @@ -77,7 +72,6 @@ public static function setUpBeforeClass(): void protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); - $this->model = $this->objectManager->create(\Magento\CatalogImportExport\Model\Import\Product::class); } /** @@ -397,15 +391,15 @@ private function doImport( bool $validateOnly = false ): ProcessingErrorAggregatorInterface { /** @var Filesystem $filesystem */ - $filesystem =$this->objectManager->create(Filesystem::class); + $filesystem = $this->objectManager->create(Filesystem::class); $directoryWrite = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = ImportAdapter::findAdapterFor($file, $directoryWrite); - $errors = $this->model - ->setParameters(['behavior' => $behavior, 'entity' => 'catalog_product']) - ->setSource($source) - ->validateData(); + $model = $this->objectManager->create(\Magento\CatalogImportExport\Model\Import\Product::class); + $model->setParameters(['behavior' => $behavior, 'entity' => 'catalog_product']); + $model->setSource($source); + $errors = $model->validateData(); if (!$validateOnly && !$errors->getAllErrors()) { - $this->model->importData(); + $model->importData(); } return $errors; } 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 1dcf94d2fd20..c436f175be4b 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 4526a83bb0bc..af81c060f3cc 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 3bfd90cd35a3..e1a41c665bbf 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(); @@ -395,7 +387,6 @@ private function updateCategorySortBy( * @magentoDataFixture Magento/Catalog/_files/products_with_not_empty_layered_navigation_attribute.php * @magentoDataFixture Magento/Framework/Search/_files/product_configurable_with_out-of-stock_child.php * @magentoConfigFixture current_store cataloginventory/options/show_out_of_stock 1 - * @magentoConfigFixture default/catalog/search/engine elasticsearch7 * @dataProvider productListWithOutOfStockSortOrderDataProvider * @param string $sortBy * @param string $direction @@ -416,7 +407,6 @@ public function testProductListOutOfStockSortOrderWithElasticsearch( * @magentoDataFixture Magento/Catalog/_files/products_with_not_empty_layered_navigation_attribute.php * @magentoDataFixture Magento/Framework/Search/_files/product_configurable_with_out-of-stock_child.php * @magentoConfigFixture current_store cataloginventory/options/show_out_of_stock 1 - * @magentoConfigFixture default/catalog/search/engine mysql * @dataProvider productListWithOutOfStockSortOrderDataProvider * @param string $sortBy * @param string $direction @@ -473,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 9d3f11eb1247..390ad0117388 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 c53ee2170d4b..6ee6c8a2b7e7 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 957b5e9325da..eb19b7534b5b 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 4f6f7bfb4aac..000000000000 --- 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 bde86b3b3544..d14eb924ac56 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 5b9266dc1137..0e454a854d8f 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 a3b2862aa2d2..47d03741bbd6 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 24d5b668ac09..e5cc7082b65d 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 9d388dfac3a9..5d1fff9f62fe 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 11c9c6166e07..cb698d8fc659 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 46281e721b07..bb108040fa08 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 66cd4e352764..bd9fddffd844 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 9ae327036971..f94b9c6db54a 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 0e30015d9816..868a568fca27 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 fd33ddf78ff3..5e9b64d12a72 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 000000000000..fbec477036eb --- /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 9979e8cd6ea6..8c32cb192ad3 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 6803d96f0c0d..fe61b3e197ca 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 d5c9e4bc8a5a..df991671cbb3 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 0ed731776205..e6096877aa72 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 c01d5dbbdd04..ebf19bb5e11f 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 2b1b271a8bb3..c8460cf03a8a 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 eef2a371ce68..a40103ee5d27 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 000000000000..1347ed9b4393 --- /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/AbstractProductExportImportTestCase.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php index 0953c4d4bdef..2dc6cdd2f894 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php @@ -408,7 +408,6 @@ private function exportProducts(Product $exportProduct = null) { $csvfile = uniqid('importexport_') . '.csv'; $this->csvFile = $csvfile; - $exportProduct = $exportProduct ?: $this->objectManager->create( Product::class ); @@ -419,10 +418,8 @@ private function exportProducts(Product $exportProduct = null) $exportProduct->setWriter($writer); $content = $exportProduct->export(); $this->assertNotEmpty($content); - $directory = $this->fileSystem->getDirectoryWrite(DirectoryList::VAR_IMPORT_EXPORT); $directory->getDriver()->filePutContents($directory->getAbsolutePath($csvfile), $content); - return $csvfile; } @@ -447,15 +444,12 @@ private function importProducts(string $csvfile, string $behavior): void 'directory' => $directory ] ); - $appParams = \Magento\TestFramework\Helper\Bootstrap::getInstance()->getBootstrap() ->getApplication() ->getInitParams()[Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS]; $mediaDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::MEDIA); - $mediaDir = $mediaDirectory->getDriver() instanceof File ? $appParams[DirectoryList::MEDIA][DirectoryList::PATH] : 'media'; - $mediaDirectory->create('catalog/product'); $mediaDirectory->create('import'); $importModel->setParameters( @@ -466,17 +460,13 @@ private function importProducts(string $csvfile, string $behavior): void $uploader = $importModel->getUploader(); $this->assertTrue($uploader->setDestDir($mediaDir . '/catalog/product')); $this->assertTrue($uploader->setTmpDir($mediaDir . '/import')); - - $errors = $importModel->setParameters( - [ - 'behavior' => $behavior, - 'entity' => 'catalog_product', - ] - )->setSource( - $source - )->validateData(); + $importModel->setParameters([ + 'behavior' => $behavior, + 'entity' => 'catalog_product', + ]); + $importModel->setSource($source); + $errors = $importModel->validateData(); $errorMessage = $this->extractErrorMessage($errors->getAllErrors()); - $this->assertEmpty( $errorMessage, 'Product import from file ' . $csvfile . ' validation errors: ' . $errorMessage @@ -502,7 +492,6 @@ private function extractErrorMessage(array $errors): string foreach ($errors as $error) { $errorMessage = "\n" . $error->getErrorMessage(); } - return $errorMessage; } 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 14283de9a071..f48cdc501d39 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 c7e82bb62823..a64bfdbbab82 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 000000000000..a8d96a246fc9 --- /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/ProductCategoriesTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductCategoriesTest.php index b6e9f4462ac8..1d8c9fb2c643 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductCategoriesTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductCategoriesTest.php @@ -51,19 +51,15 @@ public function testProductCategories($fixture, $separator) 'directory' => $directory ] ); - $errors = $this->_model->setSource( - $source - )->setParameters( - [ - 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, - 'entity' => 'catalog_product', - Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR => $separator - ] - )->validateData(); - + $this->_model->setSource($source); + $this->_model->setParameters([ + 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, + 'entity' => 'catalog_product', + Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR => $separator + ]); + $errors = $this->_model->validateData(); $this->assertTrue($errors->getErrorsCount() == 0); $this->_model->importData(); - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $resource = $objectManager->get(\Magento\Catalog\Model\ResourceModel\Product::class); $productId = $resource->getIdBySku('simple1'); @@ -89,10 +85,8 @@ public function testProductPositionInCategory() $collection->addNameToResult()->load(); /** @var Category $category */ $category = $collection->getItemByColumnValue('name', 'Category 1'); - /** @var ProductRepositoryInterface $productRepository */ $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); - $categoryProducts = []; $i = 51; foreach (['simple1', 'simple2', 'simple3'] as $sku) { @@ -100,11 +94,9 @@ public function testProductPositionInCategory() } $category->setPostedProducts($categoryProducts); $category->save(); - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Framework\Filesystem::class ); - $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( \Magento\ImportExport\Model\Import\Source\Csv::class, @@ -113,18 +105,14 @@ public function testProductPositionInCategory() 'directory' => $directory ] ); - $errors = $this->_model->setSource( - $source - )->setParameters( - [ - 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, - 'entity' => 'catalog_product' - ] - )->validateData(); - + $this->_model->setSource($source); + $this->_model->setParameters([ + 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, + 'entity' => 'catalog_product' + ]); + $errors = $this->_model->validateData(); $this->assertTrue($errors->getErrorsCount() == 0); $this->_model->importData(); - /** @var \Magento\Framework\App\ResourceConnection $resourceConnection */ $resourceConnection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Framework\App\ResourceConnection::class 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 000000000000..7a214fc73305 --- /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 b3ce40437c53..6e2e267a7182 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 @@ -7,10 +7,19 @@ namespace Magento\CatalogImportExport\Model\Import\ProductTest; +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Helper\Data as CatalogConfig; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; use Magento\CatalogImportExport\Model\Import\ProductTestBase; +use Magento\ImportExport\Helper\Data as ImportExportConfig; +use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Test\Fixture\Store as StoreFixture; +use Magento\TestFramework\Fixture\AppIsolation; +use Magento\TestFramework\Fixture\Config; +use Magento\TestFramework\Fixture\DataFixture; /** * Integration test for \Magento\CatalogImportExport\Model\Import\Product class. @@ -125,7 +134,9 @@ public function testSaveCustomOptions(string $importFile, string $sku, int $expe // Make sure that after importing existing options again, option IDs and option value IDs are not changed $customOptionValues = $this->getCustomOptionValues($sku); - $this->createImportModel($pathToFile)->importData(); + $importModel = $this->createImportModel($pathToFile); + $importModel->validateData(); + $importModel->importData(); $this->assertEquals($customOptionValues, $this->getCustomOptionValues($sku)); // Cleanup imported products @@ -138,97 +149,142 @@ public function testSaveCustomOptions(string $importFile, string $sku, int $expe /** * Tests adding of custom options with multiple store views * - * @magentoConfigFixture current_store catalog/price/scope 1 - * @magentoDataFixture Magento/Store/_files/core_second_third_fixturestore.php + * @dataProvider saveCustomOptionsWithMultipleStoreViewsDataProvider + * @param string $importFile + * @param array $expected */ - public function testSaveCustomOptionsWithMultipleStoreViews() - { + #[ + AppIsolation(true), + Config(CatalogConfig::XML_PATH_PRICE_SCOPE, CatalogConfig::PRICE_SCOPE_WEBSITE, ScopeInterface::SCOPE_STORE), + DataFixture(StoreFixture::class, ['code' => 'secondstore']), + DataFixture( + ProductFixture::class, + [ + 'sku' => 'simple2', + 'options' => [ + [ + 'type' => ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN, + 'title' => 'Option 1', + 'values' => [ + [ + 'title' => 'Option 1 Value 1', + 'price' => 2.5, + 'sku' => 'option1value1', + ], + [ + 'title' => 'Option 1 Value 2', + 'price' => 3, + 'sku' => 'option1value2', + ], + ] + ] + ] + ] + ), + ] + public function testSaveCustomOptionsWithMultipleStoreViews( + string $importFile, + array $expected + ) { + $expected = $this->getFullExpectedOptions($expected); $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var StoreManagerInterface $storeManager */ $storeManager = $objectManager->get(StoreManagerInterface::class); - $storeCodes = [ - 'admin', - 'default', - 'secondstore', - ]; - /** @var StoreManagerInterface $storeManager */ - $importFile = 'product_with_custom_options_and_multiple_store_views.csv'; - $sku = 'simple'; $pathToFile = __DIR__ . '/../_files/' . $importFile; $importModel = $this->createImportModel($pathToFile); $errors = $importModel->validateData(); $this->assertTrue($errors->getErrorsCount() == 0, 'Import File Validation Failed'); $importModel->importData(); /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $productRepository = $objectManager->get( \Magento\Catalog\Api\ProductRepositoryInterface::class ); - foreach ($storeCodes as $storeCode) { - $storeManager->setCurrentStore($storeCode); - $product = $productRepository->get($sku); - $options = $product->getOptionInstance()->getProductOptions($product); - $expectedData = $this->getExpectedOptionsData($pathToFile, $storeCode); - $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->assertEquals( - $expectedOptions, - $actualOptions, - 'Expected and actual options arrays does not match' - ); - - // assert of options data - $this->assertCount( - count($expectedData['data']), - $actualData['data'], - 'Expected and actual data count does not match' - ); - $this->assertCount( - count($expectedData['values']), - $actualData['values'], - 'Expected and actual values count does not match' - ); - - foreach ($expectedData['options'] as $expectedId => $expectedOption) { - $elementExist = false; - // find value in actual options and values - foreach ($actualData['options'] as $actualId => $actualOption) { - if ($actualOption == $expectedOption) { - $elementExist = true; - $this->assertEquals( - $expectedData['data'][$expectedId], - $actualData['data'][$actualId], - 'Expected data does not match actual data' - ); - if (array_key_exists($expectedId, $expectedData['values'])) { - $this->assertEquals( - $expectedData['values'][$expectedId], - $actualData['values'][$actualId], - 'Expected values does not match actual data' - ); - } - unset($actualData['options'][$actualId]); - // remove value in case of duplicating key values - break; + $actual = []; + foreach ($expected as $sku => $storesData) { + foreach (array_keys($storesData) as $storeCode) { + $product = $productRepository->get($sku, false, $storeManager->getStore($storeCode)->getId(), true); + $options = $product->getOptionInstance()->getProductOptions($product); + $actual[$sku][$storeCode] = []; + /** @var $option \Magento\Catalog\Model\Product\Option */ + foreach ($options as $option) { + $optionData = [ + 'type' => $option->getType(), + 'title' => $option->getTitle() + ]; + $optionData += $this->getOptionData($option); + if (in_array($option->getType(), $this->specificTypes)) { + $optionData['values'] = $this->getOptionValues($option); } + $actual[$sku][$storeCode][] = $optionData; } - $this->assertTrue($elementExist, 'Element must exist.'); } + } + + $this->assertEquals($expected, $actual); + + // Make sure that after importing existing options again, option IDs and option value IDs are not changed + $expectedIds = []; + $actualIds = []; + foreach (array_keys($expected) as $sku) { + $expectedIds[$sku] = $this->getCustomOptionValues($sku); + } + $importModel = $this->createImportModel($pathToFile); + $importModel->validateData(); + $importModel->importData(); + foreach (array_keys($expected) as $sku) { + $actualIds[$sku] = $this->getCustomOptionValues($sku); - // Make sure that after importing existing options again, option IDs and option value IDs are not changed - $customOptionValues = $this->getCustomOptionValues($sku); - $this->createImportModel($pathToFile)->importData(); - $this->assertEquals( - $customOptionValues, - $this->getCustomOptionValues($sku), - 'Option IDs changed after second import' - ); } + + $this->assertEquals( + $expectedIds, + $actualIds, + 'Option IDs changed after second import' + ); + } + + /** + * Tests adding of custom options with multiple store views across bunches + * + * @dataProvider saveCustomOptionsWithMultipleStoreViewsDataProvider + * @param string $importFile + * @param array $expected + */ + #[ + AppIsolation(true), + Config(CatalogConfig::XML_PATH_PRICE_SCOPE, CatalogConfig::PRICE_SCOPE_WEBSITE, ScopeInterface::SCOPE_STORE), + Config(ImportExportConfig::XML_PATH_BUNCH_SIZE, 2, ScopeInterface::SCOPE_STORE), + DataFixture(StoreFixture::class, ['code' => 'secondstore']), + DataFixture( + ProductFixture::class, + [ + 'sku' => 'simple2', + 'options' => [ + [ + 'type' => ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN, + 'title' => 'Option 1', + 'values' => [ + [ + 'title' => 'Option 1 Value 1', + 'price' => 2.5, + 'sku' => 'option1value1', + ], + [ + 'title' => 'Option 1 Value 2', + 'price' => 3, + 'sku' => 'option1value2', + ], + ] + ] + ] + ] + ), + ] + public function testSaveCustomOptionsWithMultipleStoreViewsAcrossMultipleBunches( + string $importFile, + array $expected + ) { + $this->testSaveCustomOptionsWithMultipleStoreViews($importFile, $expected); } /** @@ -255,6 +311,401 @@ public function getBehaviorDataProvider(): array ]; } + /** + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function saveCustomOptionsWithMultipleStoreViewsDataProvider(): array + { + return [ + [ + 'product_with_custom_options_and_multiple_store_views.csv', + [ + 'simple' => [ + 'admin' => [ + [ + 'title' => 'Test Field Title', + 'type' => 'field', + 'is_require' => '1', + 'sku' => '1-text', + 'price' => '100.000000', + 'max_characters' => '0', + 'sort_order' => '1', + ], + [ + 'title' => 'Test Date and Time Title', + 'type' => 'date_time', + 'is_require' => '1', + 'sku' => '2-date', + 'price' => '200.000000', + 'max_characters' => '0', + 'sort_order' => '2', + ], + [ + 'title' => 'Test Select', + 'type' => 'drop_down', + 'is_require' => '1', + 'sku' => '', + 'price' => null, + 'max_characters' => '0', + 'sort_order' => '3', + 'values' => [ + [ + 'title' => 'Select Option 1', + 'sku' => '3-1-select', + 'price' => '310.000000', + ], + [ + 'title' => 'Select Option 2', + 'sku' => '3-2-select', + 'price' => '320.000000', + ] + ] + ], + [ + 'title' => 'Test Checkbox', + 'type' => 'checkbox', + 'is_require' => '1', + 'sku' => '', + 'price' => null, + 'max_characters' => '0', + 'sort_order' => '4', + 'values' => [ + [ + 'title' => 'Checkbox Option 1', + 'sku' => '4-1-select', + 'price' => '410.000000', + ], + [ + 'title' => 'Checkbox Option 2', + 'sku' => '4-2-select', + 'price' => '420.000000', + ] + ] + ], + [ + 'title' => 'Test Radio', + 'type' => 'radio', + 'is_require' => '1', + 'sku' => '', + 'price' => null, + 'max_characters' => '0', + 'sort_order' => '5', + 'values' => [ + [ + 'title' => 'Radio Option 1', + 'sku' => '5-1-radio', + 'price' => '510.000000', + ], + [ + 'title' => 'Radio Option 2', + 'sku' => '5-2-radio', + 'price' => '520.000000', + ] + ] + ] + ], + 'default' => [ + [ + 'title' => 'Test Field Title_default', + ], + [ + 'title' => 'Test Date and Time Title_default', + ], + [ + 'title' => 'Test Select_default', + 'values' => [ + [ + 'title' => 'Select Option 1_default', + ], + [ + 'title' => 'Select Option 2_default', + ] + ] + ], + [ + 'title' => 'Test Checkbox_default', + 'values' => [ + [ + 'title' => 'Checkbox Option 1_default', + ], + [ + 'title' => 'Checkbox Option 2_default', + ] + ] + ], + [ + 'title' => 'Test Radio_default', + 'values' => [ + [ + 'title' => 'Radio Option 1_default', + ], + [ + 'title' => 'Radio Option 2_default', + ] + ] + ] + ], + 'secondstore' => [ + [ + 'title' => 'Test Field Title_fixture_second_store', + 'price' => '101.000000' + ], + [ + 'title' => 'Test Date and Time Title_fixture_second_store', + 'price' => '201.000000' + ], + [ + 'title' => 'Test Select_fixture_second_store', + 'values' => [ + [ + 'title' => 'Select Option 1_fixture_second_store', + 'price' => '311.000000' + ], + [ + 'title' => 'Select Option 2_fixture_second_store', + 'price' => '321.000000' + ] + ] + ], + [ + 'title' => 'Test Checkbox_second_store', + 'values' => [ + [ + 'title' => 'Checkbox Option 1_second_store', + 'price' => '411.000000' + ], + [ + 'title' => 'Checkbox Option 2_second_store', + 'price' => '421.000000' + ] + ] + ], + [ + 'title' => 'Test Radio_fixture_second_store', + 'values' => [ + [ + 'title' => 'Radio Option 1_fixture_second_store', + 'price' => '511.000000' + ], + [ + 'title' => 'Radio Option 2_fixture_second_store', + 'price' => '521.000000' + ] + ] + ] + ], + ], + 'newprod2' => [ + 'admin' => [], + 'default' => [], + 'secondstore' => [], + ], + 'newprod3' => [ + 'admin' => [ + [ + 'title' => 'Line 1', + 'type' => 'field', + 'is_require' => '1', + 'sku' => '', + 'price' => null, + 'max_characters' => '30', + 'sort_order' => '1', + ], + [ + 'title' => 'Line 2', + 'type' => 'field', + 'is_require' => '0', + 'sku' => '', + 'price' => null, + 'max_characters' => '30', + 'sort_order' => '2', + ], + ], + 'default' => [ + [ + 'title' => 'Line 1', + 'type' => 'field', + 'is_require' => '1', + 'sku' => '', + 'price' => null, + 'max_characters' => '30', + 'sort_order' => '1', + ], + [ + 'title' => 'Line 2', + 'type' => 'field', + 'is_require' => '0', + 'sku' => '', + 'price' => null, + 'max_characters' => '30', + 'sort_order' => '2', + ], + ], + 'secondstore' => [ + [ + 'title' => 'Line 1', + 'type' => 'field', + 'is_require' => '1', + 'sku' => '', + 'price' => null, + 'max_characters' => '30', + 'sort_order' => '1', + ], + [ + 'title' => 'Line 2', + 'type' => 'field', + 'is_require' => '0', + 'sku' => '', + 'price' => null, + 'max_characters' => '30', + 'sort_order' => '2', + ], + ], + ], + 'newprod4' => [ + 'admin' => [], + 'default' => [], + 'secondstore' => [], + ], + 'newprod5' => [ + 'admin' => [ + [ + 'title' => 'Line 3', + 'type' => 'field', + 'is_require' => '1', + 'sku' => '', + 'price' => null, + 'max_characters' => '30', + 'sort_order' => '1', + ], + [ + 'title' => 'Line 4', + 'type' => 'field', + 'is_require' => '0', + 'sku' => '', + 'price' => null, + 'max_characters' => '30', + 'sort_order' => '2', + ], + ], + 'default' => [ + [ + 'title' => 'Line 3', + 'type' => 'field', + 'is_require' => '1', + 'sku' => '', + 'price' => null, + 'max_characters' => '30', + 'sort_order' => '1', + ], + [ + 'title' => 'Line 4', + 'type' => 'field', + 'is_require' => '0', + 'sku' => '', + 'price' => null, + 'max_characters' => '30', + 'sort_order' => '2', + ], + ], + 'secondstore' => [ + [ + 'title' => 'Line 3', + 'type' => 'field', + 'is_require' => '1', + 'sku' => '', + 'price' => null, + 'max_characters' => '30', + 'sort_order' => '1', + ], + [ + 'title' => 'Line 4', + 'type' => 'field', + 'is_require' => '0', + 'sku' => '', + 'price' => null, + 'max_characters' => '30', + 'sort_order' => '2', + ], + ], + ], + 'simple2' => [ + 'admin' => [ + [ + 'title' => 'Option 1', + 'type' => 'drop_down', + 'is_require' => '1', + 'sku' => '', + 'price' => null, + 'max_characters' => '0', + 'sort_order' => '1', + 'values' => [ + [ + 'title' => 'Option 1 Value 1', + 'sku' => 'option1value1', + 'price' => '1.200000', + ], + [ + 'title' => 'Option 1 Value 2', + 'sku' => 'option1value2', + 'price' => '1.400000', + ] + ] + ] + ], + 'default' => [ + [ + 'title' => 'Option 1 Store1', + 'type' => 'drop_down', + 'is_require' => '1', + 'sku' => '', + 'price' => null, + 'max_characters' => '0', + 'sort_order' => '1', + 'values' => [ + [ + 'title' => 'Option 1 Value 1 Store1', + 'sku' => 'option1value1', + 'price' => '1.100000', + ], + [ + 'title' => 'Option 1 Value 2 Store1', + 'sku' => 'option1value2', + 'price' => '1.300000', + ] + ] + ] + ], + 'secondstore' => [ + [ + 'title' => 'Option 1 Store2', + 'type' => 'drop_down', + 'is_require' => '1', + 'sku' => '', + 'price' => null, + 'max_characters' => '0', + 'sort_order' => '1', + 'values' => [ + [ + 'title' => 'Option 1 Value 1 Store2', + 'sku' => 'option1value1', + 'price' => '1.000000', + ], + [ + 'title' => 'Option 1 Value 2 Store2', + 'sku' => 'option1value2', + 'price' => '1.200000', + ] + ] + ] + ], + ] + ] + ] + ]; + } + /** * @param string $productSku * @return array ['optionId' => ['optionValueId' => 'optionValueTitle', ...], ...] @@ -484,4 +935,94 @@ protected function getOptionValues(\Magento\Catalog\Model\Product\Option $option return false; } + + /** + * @param array $expected + * @return array + */ + private function getFullExpectedOptions(array $expected): array + { + foreach ($expected as &$data) { + foreach ($data as $store => &$options) { + if ($store !== 'admin') { + foreach ($options as $optKey => &$option) { + $option += $data['admin'][$optKey]; + if (isset($option['values'])) { + foreach ($option['values'] as $valKey => &$value) { + $value += $data['admin'][$optKey]['values'][$valKey]; + } + } + } + } + } + } + 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 a98702403276..30bddb975174 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; /** @@ -54,11 +64,9 @@ public function testSaveProductsVisibility() $product->load($productId); $productsBeforeImport[] = $product; } - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); - $source = $this->objectManager->create( \Magento\ImportExport\Model\Import\Source\Csv::class, [ @@ -66,16 +74,13 @@ public function testSaveProductsVisibility() 'directory' => $directory ] ); - $errors = $this->_model->setParameters( + $this->_model->setParameters( ['behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, 'entity' => 'catalog_product'] - )->setSource( - $source - )->validateData(); - + ); + $this->_model->setSource($source); + $errors = $this->_model->validateData(); $this->assertTrue($errors->getErrorsCount() == 0); - $this->_model->importData(); - /** @var $productBeforeImport \Magento\Catalog\Model\Product */ foreach ($productsBeforeImport as $productBeforeImport) { /** @var $productAfterImport \Magento\Catalog\Model\Product */ @@ -83,12 +88,8 @@ public function testSaveProductsVisibility() \Magento\Catalog\Model\Product::class ); $productAfterImport->load($productBeforeImport->getId()); - $this->assertEquals($productBeforeImport->getVisibility(), $productAfterImport->getVisibility()); - unset($productAfterImport); } - - unset($productsBeforeImport, $product); } /** @@ -115,11 +116,9 @@ public function testSaveDatetimeAttribute() $product->load($productId); $productsBeforeImport[$product->getSku()] = $product; } - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); - $source = $this->objectManager->create( \Magento\ImportExport\Model\Import\Source\Csv::class, [ @@ -127,16 +126,13 @@ public function testSaveDatetimeAttribute() 'directory' => $directory ] ); - $errors = $this->_model->setParameters( + $this->_model->setParameters( ['behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, 'entity' => 'catalog_product'] - )->setSource( - $source - )->validateData(); - + ); + $this->_model->setSource($source); + $errors = $this->_model->validateData(); $this->assertTrue($errors->getErrorsCount() == 0); - $this->_model->importData(); - $source->rewind(); foreach ($source as $row) { /** @var $productAfterImport \Magento\Catalog\Model\Product */ @@ -186,7 +182,6 @@ public function testProductWithLinks() $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Framework\Filesystem::class ); - $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( \Magento\ImportExport\Model\Import\Source\Csv::class, @@ -195,18 +190,14 @@ public function testProductWithLinks() 'directory' => $directory ] ); - $errors = $this->_model->setSource( - $source - )->setParameters( - [ - 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, - 'entity' => 'catalog_product' - ] - )->validateData(); - + $this->_model->setSource($source); + $this->_model->setParameters([ + 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, + 'entity' => 'catalog_product' + ]); + $errors = $this->_model->validateData(); $this->assertTrue($errors->getErrorsCount() == 0); $this->_model->importData(); - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $resource = $objectManager->get(\Magento\Catalog\Model\ResourceModel\Product::class); $productId = $resource->getIdBySku('simple4'); @@ -238,9 +229,7 @@ public function testProductWithLinks() public function testUpdateUrlRewritesOnImport() { $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); - $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); - $source = $this->objectManager->create( \Magento\ImportExport\Model\Import\Source\Csv::class, [ @@ -248,19 +237,14 @@ public function testUpdateUrlRewritesOnImport() 'directory' => $directory ] ); - $errors = $this->_model->setParameters( - [ - 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, - 'entity' => \Magento\Catalog\Model\Product::ENTITY - ] - )->setSource( - $source - )->validateData(); - + $this->_model->setParameters([ + 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, + 'entity' => \Magento\Catalog\Model\Product::ENTITY + ]); + $this->_model->setSource($source); + $errors = $this->_model->validateData(); $this->assertTrue($errors->getErrorsCount() == 0); - $this->_model->importData(); - /** @var \Magento\Catalog\Model\Product $product */ $product = $this->objectManager->create(\Magento\Catalog\Model\ProductRepository::class)->get('simple'); $listOfProductUrlKeys = [ @@ -271,7 +255,6 @@ public function testUpdateUrlRewritesOnImport() $repUrlRewriteCol = $this->objectManager->create( UrlRewriteCollection::class ); - /** @var UrlRewriteCollection $collUrlRewrite */ $collUrlRewrite = $repUrlRewriteCol->addFieldToSelect(['request_path']) ->addFieldToFilter('entity_id', ['eq'=> $product->getEntityId()]) @@ -279,7 +262,6 @@ public function testUpdateUrlRewritesOnImport() ->load(); $listOfUrlRewriteIds = $collUrlRewrite->getAllIds(); $this->assertCount(3, $collUrlRewrite); - foreach ($listOfUrlRewriteIds as $key => $id) { $this->assertEquals( $listOfProductUrlKeys[$key], @@ -297,9 +279,7 @@ public function testUpdateUrlRewritesOnImport() public function testUpdateUrlRewritesOnImportWithoutGenerateCategoryProductRewrites() { $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); - $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); - $source = $this->objectManager->create( \Magento\ImportExport\Model\Import\Source\Csv::class, [ @@ -307,19 +287,14 @@ public function testUpdateUrlRewritesOnImportWithoutGenerateCategoryProductRewri 'directory' => $directory ] ); - $errors = $this->_model->setParameters( - [ - 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, - 'entity' => \Magento\Catalog\Model\Product::ENTITY - ] - )->setSource( - $source - )->validateData(); - + $this->_model->setParameters([ + 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, + 'entity' => \Magento\Catalog\Model\Product::ENTITY + ]); + $this->_model->setSource($source); + $errors = $this->_model->validateData(); $this->assertTrue($errors->getErrorsCount() == 0); - $this->_model->importData(); - /** @var \Magento\Catalog\Model\Product $product */ $product = $this->objectManager->create(\Magento\Catalog\Model\ProductRepository::class)->get('simple'); $listOfProductUrlKeys = [ @@ -330,7 +305,6 @@ public function testUpdateUrlRewritesOnImportWithoutGenerateCategoryProductRewri $repUrlRewriteCol = $this->objectManager->create( UrlRewriteCollection::class ); - /** @var UrlRewriteCollection $collUrlRewrite */ $collUrlRewrite = $repUrlRewriteCol->addFieldToSelect(['request_path']) ->addFieldToFilter('entity_id', ['eq'=> $product->getEntityId()]) @@ -338,7 +312,6 @@ public function testUpdateUrlRewritesOnImportWithoutGenerateCategoryProductRewri ->load(); $listOfUrlRewriteIds = $collUrlRewrite->getAllIds(); $this->assertCount(1, $collUrlRewrite); - foreach ($listOfUrlRewriteIds as $key => $id) { $this->assertEquals( $listOfProductUrlKeys[$key], @@ -607,7 +580,7 @@ public function testImportWithDifferentSkuCase() $this->assertTrue($errors->getErrorsCount() == 0); $this->_model->importData(); - + $this->createNewModel(); $this->assertCount( 3, $productRepository->getList($searchCriteria)->getItems() @@ -670,14 +643,11 @@ public function testCheckDoubleImportOfProducts() 'simple2', 'simple3', ]; - /** @var SearchCriteria $searchCriteria */ $searchCriteria = $this->searchCriteriaBuilder->create(); - $this->assertTrue($this->importFile('products_with_two_store_views.csv', 2)); $productsAfterFirstImport = $this->productRepository->getList($searchCriteria)->getItems(); $this->assertCount(3, $productsAfterFirstImport); - $this->assertTrue($this->importFile('products_with_two_store_views.csv', 2)); $productsAfterSecondImport = $this->productRepository->getList($searchCriteria)->getItems(); $this->assertCount(3, $productsAfterSecondImport); @@ -714,7 +684,6 @@ public function testImportProductsWithLinksInDifferentBunches() ]; $pathToFile = __DIR__ . '/../_files/products_to_import_with_related.csv'; $filesystem = $this->objectManager->create(Filesystem::class); - $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( Csv::class, @@ -723,18 +692,14 @@ public function testImportProductsWithLinksInDifferentBunches() 'directory' => $directory ] ); - $errors = $this->_model->setSource($source) - ->setParameters( - [ - 'behavior' => Import::BEHAVIOR_APPEND, - 'entity' => 'catalog_product' - ] - ) - ->validateData(); - + $this->_model->setSource($source); + $this->_model->setParameters([ + 'behavior' => Import::BEHAVIOR_APPEND, + 'entity' => 'catalog_product' + ]); + $errors = $this->_model->validateData(); $this->assertTrue($errors->getErrorsCount() == 0); $this->_model->importData(); - $resource = $this->objectManager->get(ProductResource::class); $productId = $resource->getIdBySku('simple6'); /** @var Product $product */ @@ -770,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 a6f1448d6131..043475c99cc3 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/ProductTestBase.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTestBase.php index 0a966cee59b5..27ce883430fe 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTestBase.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTestBase.php @@ -84,17 +84,28 @@ protected function setUp(): void $this->logger = $this->getMockBuilder(LoggerInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\CatalogImportExport\Model\Import\Product::class, - ['logger' => $this->logger] - ); + $this->createNewModel(); $this->importedProducts = []; $this->searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - parent::setUp(); } + /** + * Creates a fresh Product Import object + * + * This is needed because the object has the ids associated to its previous validations. + * + * @return void + */ + protected function createNewModel() + { + $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\CatalogImportExport\Model\Import\Product::class, + ['logger' => $this->logger] + ); + } + /** * @inheritDoc */ @@ -279,6 +290,7 @@ protected function csvToArray($content, $entityId = null) */ protected function importDataForMediaTest(string $fileName, int $expectedErrors = 0) { + $this->createNewModel(); $filesystem = $this->objectManager->get(Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( @@ -294,7 +306,6 @@ protected function importDataForMediaTest(string $fileName, int $expectedErrors $tmpDir = $mediaDirPath . DIRECTORY_SEPARATOR . 'import' . DIRECTORY_SEPARATOR . 'images'; $mediaDirectory->create('catalog' . DIRECTORY_SEPARATOR . 'product'); $mediaDirectory->create('import' . DIRECTORY_SEPARATOR . 'images'); - $this->_model->setParameters( [ 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, @@ -305,11 +316,9 @@ protected function importDataForMediaTest(string $fileName, int $expectedErrors $uploader = $this->_model->getUploader(); $this->assertTrue($uploader->setDestDir($destDir)); $this->assertTrue($uploader->setTmpDir($tmpDir)); - $errors = $this->_model->setSource( - $source - )->validateData(); + $this->_model->setSource($source); + $errors = $this->_model->validateData(); $this->assertTrue($errors->getErrorsCount() == 0); - $this->_model->importData(); $this->assertEquals( $expectedErrors, @@ -377,19 +386,15 @@ protected function importFile(string $fileName, int $bunchSize = 100): bool ); $mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); $mediaDirectory->create('import'); - $errors = $this->_model->setParameters( - [ - 'behavior' => Import::BEHAVIOR_APPEND, - 'entity' => 'catalog_product', - Import::FIELDS_ENCLOSURE => 1, - Import::FIELD_NAME_IMG_FILE_DIR => $this->getMediaDirPath($mediaDirectory) . '/import' - ] - ) - ->setSource($source) - ->validateData(); - + $this->_model->setParameters([ + 'behavior' => Import::BEHAVIOR_APPEND, + 'entity' => 'catalog_product', + Import::FIELDS_ENCLOSURE => 1, + Import::FIELD_NAME_IMG_FILE_DIR => $this->getMediaDirPath($mediaDirectory) . '/import' + ]); + $this->_model->setSource($source); + $errors = $this->_model->validateData(); $this->assertTrue($errors->getErrorsCount() === 0); - return $this->_model->importData(); } 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 000000000000..bc24fe9c6421 --- /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/CatalogImportExport/Model/Import/_files/product_with_custom_options_and_multiple_store_views.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_with_custom_options_and_multiple_store_views.csv index 0d4c53ca5812..6aefef5ed836 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_with_custom_options_and_multiple_store_views.csv +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_with_custom_options_and_multiple_store_views.csv @@ -1,8 +1,11 @@ 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,secondwebsite",,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,,default,Default,simple,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"name=Test Field Title_default,type=field,sku=1-text|name=Test Date and Time Title_default,type=date_time,sku=2-date|name=Test Select_default,type=drop_down,sku=3-1-select,option_title=Select Option 1_default|name=Test Select_default,type=drop_down,sku=3-2-select,option_title=Select Option 2_default|name=Test Checkbox_default,type=checkbox,sku=4-1-select,option_title=Checkbox Option 1_default|name=Test Checkbox_default,type=checkbox,sku=4-2-select,option_title=Checkbox Option 2_default|name=Test Radio_default,type=radio,sku=5-1-radio,option_title=Radio Option 1_default|name=Test Radio_default,type=radio,sku=5-2-radio,option_title=Radio Option 2_default",,,,,,,,,,,,,,,,,,,,,,,,,,, -simple,,secondstore,Default,simple,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"name=Test Field Title_fixture_second_store,type=field,sku=1-text,price=101.000000|name=Test Date and Time Title_fixture_second_store,type=date_time,sku=2-date,price=201.000000|name=Test Select_fixture_second_store,type=drop_down,sku=3-1-select,price=311.000000,option_title=Select Option 1_fixture_second_store|name=Test Select_fixture_second_store,type=drop_down,sku=3-2-select,price=321.000000,option_title=Select Option 2_fixture_second_store|name=Test Checkbox_second_store,type=checkbox,sku=4-1-select,price=411.000000,option_title=Checkbox Option 1_second_store|name=Test Checkbox_second_store,type=checkbox,sku=4-2-select,price=421.000000,option_title=Checkbox Option 2_second_store|name=Test Radio_fixture_second_store,type=radio,sku=5-1-radio,price=511.000000,option_title=Radio Option 1_fixture_second_store|name=Test Radio_fixture_second_store,type=radio,sku=5-2-radio,price=521.000000,option_title=Radio Option 2_fixture_second_store",,,,,,,,,,,,,,,,,,,,,,,,,,, -newprod2,base,secondstore,Default,configurable,New Product 2,,,9,1,"Catalog, Search","base,secondwebsite",,10,,,,Taxable Goods,new-product-2,,,,,,,,,,,,,,,,,,,,,,,,,,1,1,999,0,0,0,1,10000,1,1,0,0,,,,,,,,,,,Block after Info Column,,, -newprod3,base,,Default,configurable,New Product 3,,,9,1,"Catalog, Search","base,secondwebsite",,10,,,,Taxable Goods,new-product-3,,,,,,,,,,,,,,,,,,,,,,,,"name=Line 1,type=field,max_characters=30,required=1,option_title=Line 1|name=Line 2,type=field,max_characters=30,required=0,option_title=Line 2",,1,1,999,0,0,0,1,10000,1,1,0,0,,,,,,,,,,,Block after Info Column,,, -newprod4,base,secondstore,Default,configurable,New Product 4,,,9,1,"Catalog, Search","base,secondwebsite",,10,,,,Taxable Goods,new-product-4,,,,,,,,,,,,,,,,,,,,,,,,,,1,1,999,0,0,0,1,10000,1,1,0,0,,,,,,,,,,,Block after Info Column,,, -newprod5,base,,Default,configurable,New Product 5,,,9,1,"Catalog, Search","base,secondwebsite",,10,,,,Taxable Goods,new-product-5,,,,,,,,,,,,,,,,,,,,,,,,"name=Line 3,type=field,max_characters=30,required=1,option_title=Line 3|name=Line 4,type=field,max_characters=30,required=0,option_title=Line 4",,1,1,999,0,0,0,1,10000,1,1,0,0,,,,,,,,,,,Block after Info Column,,, +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,,default,Default,simple,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"name=Test Field Title_default,type=field,required=1,sku=1-text|name=Test Date and Time Title_default,type=date_time,required=1,sku=2-date|name=Test Select_default,type=drop_down,required=1,sku=3-1-select,option_title=Select Option 1_default|name=Test Select_default,type=drop_down,required=1,sku=3-2-select,option_title=Select Option 2_default|name=Test Checkbox_default,type=checkbox,required=1,sku=4-1-select,option_title=Checkbox Option 1_default|name=Test Checkbox_default,type=checkbox,required=1,sku=4-2-select,option_title=Checkbox Option 2_default|name=Test Radio_default,type=radio,required=1,sku=5-1-radio,option_title=Radio Option 1_default|name=Test Radio_default,type=radio,required=1,sku=5-2-radio,option_title=Radio Option 2_default",,,,,,,,,,,,,,,,,,,,,,,,,,, +simple,,secondstore,Default,simple,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"name=Test Field Title_fixture_second_store,type=field,required=1,sku=1-text,price=101.000000|name=Test Date and Time Title_fixture_second_store,type=date_time,required=1,sku=2-date,price=201.000000|name=Test Select_fixture_second_store,type=drop_down,required=1,sku=3-1-select,price=311.000000,option_title=Select Option 1_fixture_second_store|name=Test Select_fixture_second_store,type=drop_down,required=1,sku=3-2-select,price=321.000000,option_title=Select Option 2_fixture_second_store|name=Test Checkbox_second_store,type=checkbox,required=1,sku=4-1-select,price=411.000000,option_title=Checkbox Option 1_second_store|name=Test Checkbox_second_store,type=checkbox,required=1,sku=4-2-select,price=421.000000,option_title=Checkbox Option 2_second_store|name=Test Radio_fixture_second_store,type=radio,required=1,sku=5-1-radio,price=511.000000,option_title=Radio Option 1_fixture_second_store|name=Test Radio_fixture_second_store,type=radio,required=1,sku=5-2-radio,price=521.000000,option_title=Radio Option 2_fixture_second_store",,,,,,,,,,,,,,,,,,,,,,,,,,, +newprod2,base,secondstore,Default,configurable,New Product 2,,,9,1,"Catalog, Search",base,,10,,,,Taxable Goods,new-product-2,,,,,,,,,,,,,,,,,,,,,,,,,,1,1,999,0,0,0,1,10000,1,1,0,0,,,,,,,,,,,Block after Info Column,,, +newprod3,base,,Default,configurable,New Product 3,,,9,1,"Catalog, Search",base,,10,,,,Taxable Goods,new-product-3,,,,,,,,,,,,,,,,,,,,,,,,"name=Line 1,type=field,max_characters=30,required=1,option_title=Line 1|name=Line 2,type=field,max_characters=30,required=0,option_title=Line 2",,1,1,999,0,0,0,1,10000,1,1,0,0,,,,,,,,,,,Block after Info Column,,, +newprod4,base,secondstore,Default,configurable,New Product 4,,,9,1,"Catalog, Search",base,,10,,,,Taxable Goods,new-product-4,,,,,,,,,,,,,,,,,,,,,,,,,,1,1,999,0,0,0,1,10000,1,1,0,0,,,,,,,,,,,Block after Info Column,,, +newprod5,base,,Default,configurable,New Product 5,,,9,1,"Catalog, Search",base,,10,,,,Taxable Goods,new-product-5,,,,,,,,,,,,,,,,,,,,,,,,"name=Line 3,type=field,max_characters=30,required=1,option_title=Line 3|name=Line 4,type=field,max_characters=30,required=0,option_title=Line 4",,1,1,999,0,0,0,1,10000,1,1,0,0,,,,,,,,,,,Block after Info Column,,, +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,,default,Default,simple,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"name=Option 1 Store1,type=drop_down,required=1,sku=option1value1,price=1.1,option_title=Option 1 Value 1 Store1|name=Option 1 Store1,type=drop_down,required=1,sku=option1value2,price=1.3,option_title=Option 1 Value 2 Store1",,1,1,999,0,0,0,1,10000,1,1,0,0,,,,,,,,,,,,,, +simple2,,secondstore,Default,simple,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"name=Option 1 Store2,type=drop_down,required=1,sku=option1value1,price=1.0,option_title=Option 1 Value 1 Store2|name=Option 1 Store2,type=drop_down,required=1,sku=option1value2,price=1.2,option_title=Option 1 Value 2 Store2",,1,1,999,0,0,0,1,10000,1,1,0,0,,,,,,,,,,,,,, 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 8b8aad3136e3..d086a06b94eb 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 be40a8c922f7..a1accd5e7ec6 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 e174cb33733a..3fef30f96e36 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 7593d0e8b46d..55d1d09051c5 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 000000000000..e7a02f2531fc --- /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 d71cfa1eff85..1a3696c02ac5 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 716f8d6260c4..a2f38dbd40a8 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/Model/ResourceModel/Product/ConditionsToCollectionApplierTest.php b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplierTest.php index b06de8109ecb..778c7c42249f 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplierTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplierTest.php @@ -433,6 +433,22 @@ private function conditionProvider() 'simple-product-13', ] ], + + // test filter by multiple sku and "is not one of" condition + 'variation 23' => [ + 'condition' => $this->getConditionsForVariation23(), + 'expected-sku' => [ + 'simple-product-3', + 'simple-product-4', + 'simple-product-6', + 'simple-product-7', + 'simple-product-8', + 'simple-product-9', + 'simple-product-11', + 'simple-product-12', + 'simple-product-13', + ] + ], ]; } @@ -1058,6 +1074,25 @@ private function getConditionsForVariation22() return $this->getCombineConditionFromArray($conditions); } + private function getConditionsForVariation23() + { + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'all', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '!()', + 'value' => 'simple-product-1, simple-product-2, simple-product-5, simple-product-10', + 'attribute' => 'sku' + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + private function getCombineConditionFromArray(array $data) { $combinedCondition = $this->combinedConditionFactory->create(); 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 eeb66fb923e4..2e127eb416c9 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 03e385e2dade..928dbfd7645e 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 c032f47d8834..8f4b804de157 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 000000000000..f80097ddf3be --- /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 f6d5021b4bcd..0a4711911774 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/CatalogWidget/Model/Rule/Condition/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogWidget/Model/Rule/Condition/ProductTest.php index e231251db2c3..37077d60e683 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogWidget/Model/Rule/Condition/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogWidget/Model/Rule/Condition/ProductTest.php @@ -102,11 +102,11 @@ public function testAddNonGlobalAttributeToCollectionNoProducts() $this->conditionProduct->addToCollection($collection); $collectedAttributes = $this->conditionProduct->getRule()->getCollectedAttributes(); $this->assertArrayHasKey('visibility', $collectedAttributes); - $query = (string)$collection->getSelect(); - $this->assertStringNotContainsString('visibility', $query); - $this->assertEquals('', $this->conditionProduct->getMappedSqlField()); + $this->assertEquals(0, $collection->getSize()); + $this->assertStringContainsString('visibility', (string)$this->conditionProduct->getMappedSqlField()); $this->assertFalse($this->conditionProduct->hasValueParsed()); - $this->assertFalse($this->conditionProduct->hasValue()); + $this->assertTrue($this->conditionProduct->hasValue()); + $this->assertEquals('4', $this->conditionProduct->getValue()); } /** @@ -121,9 +121,11 @@ public function testAddNonGlobalAttributeToCollection() $this->conditionProduct->addToCollection($collection); $collectedAttributes = $this->conditionProduct->getRule()->getCollectedAttributes(); $this->assertArrayHasKey('visibility', $collectedAttributes); - $query = (string)$collection->getSelect(); - $this->assertStringNotContainsString('visibility', $query); - $this->assertEquals('e.entity_id', $this->conditionProduct->getMappedSqlField()); + $this->assertEquals(1, $collection->getSize()); + $this->assertStringContainsString('visibility', (string)$this->conditionProduct->getMappedSqlField()); + $this->assertFalse($this->conditionProduct->hasValueParsed()); + $this->assertTrue($this->conditionProduct->hasValue()); + $this->assertEquals('4', $this->conditionProduct->getValue()); } /** diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php index fd89229bb73b..bcbbce7cdccb 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 000000000000..00964f5e1c5d --- /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 5f5f9dda20c6..3d6babde3624 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 14c66b67dcff..beef4f5620c1 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 4906014ad190..e573759fa41f 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 2828d2fb6cf6..80d26d6d6956 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 e99779bd9598..343ecba1ed70 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 000000000000..a6222ac17f97 --- /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 4b6fac496df0..438ba1ed75e8 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 beab52c14240..958f6da93e07 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 f1f46498d9f5..000000000000 --- 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 1a93895c8673..000000000000 --- 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 05f05311f624..310c88deb924 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 4ca8ab53ffba..237f2c95606a 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 000000000000..0b416d03694b --- /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 613d1c7f1b9a..c3d7e432df74 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 bc82c333d5d6..25cacd8961d7 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 02c2e7868938..c1cbe3de85c1 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 64fd2caeb088..f5e05453b1cd 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 b4581bf8d5da..401a74e75252 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 a5cca8fa4113..7952fefc1a10 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 86c0290edc78..c9196a581f54 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 809102007f15..1b23654e965e 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 f8eeb8edd15d..b7e2e32d9193 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 efba3be6e78c..521650738457 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 000000000000..58676013f5b1 --- /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 000000000000..19c3cf2ec909 --- /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 beccbb7b5064..859da6ec0b58 100644 --- a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerTest.php +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerTest.php @@ -8,13 +8,19 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\Indexer\Processor; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\NoSuchEntityException; -use Magento\ImportExport\Model\Import; +use Magento\Framework\Filesystem\Directory\Write as DirectoryWrite; +use Magento\Framework\Filesystem\File\WriteFactory; use Magento\Framework\Indexer\StateInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\ImportExport\Model\Import; +use Magento\ImportExport\Model\Import\Source\CsvFactory; +use Magento\TestFramework\Helper\Bootstrap; /** - * Test for class \Magento\CustomerImportExport\Model\Import\Customer which covers validation logic + * Test for class Customer which covers validation logic * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyPublicMethods) @@ -24,44 +30,54 @@ class CustomerTest extends \PHPUnit\Framework\TestCase /** * Model object which used for tests * - * @var Customer|\PHPUnit\Framework\MockObject\MockObject + * @var ObjectManagerInterface */ - protected $_model; + private $objectManager; /** - * Customer data + * Model object which used for tests * + * @var Customer&\PHPUnit\Framework\MockObject\MockObject + */ + protected $_model; + + /** * @var array */ protected $_customerData; /** - * @var \Magento\Framework\Filesystem\Directory\Write + * @var DirectoryWrite */ protected $directoryWrite; /** - * @var \Magento\Customer\Model\Indexer\Processor + * @var Processor */ private $indexerProcessor; + /** + * @var CsvFactory + */ + private $csvFactory; + + /** + * @var WriteFactory + */ + private $writeFactory; /** * Create all necessary data for tests */ protected function setUp(): void { parent::setUp(); - - $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\CustomerImportExport\Model\Import\Customer::class); + $this->objectManager = Bootstrap::getObjectManager(); + $this->_model = $this->objectManager->create(Customer::class); $this->_model->setParameters(['behavior' => Import::BEHAVIOR_ADD_UPDATE]); - $this->indexerProcessor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Customer\Model\Indexer\Processor::class); - + $this->indexerProcessor = $this->objectManager->create(\Magento\Customer\Model\Indexer\Processor::class); $propertyAccessor = new \ReflectionProperty($this->_model, 'errorMessageTemplates'); $propertyAccessor->setAccessible(true); $propertyAccessor->setValue($this->_model, []); - $this->_customerData = [ 'firstname' => 'Firstname', 'lastname' => 'Lastname', @@ -73,11 +89,10 @@ protected function setUp(): void 'website_id' => 1, 'password' => 'password', ]; - - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Framework\Filesystem::class); - $this->directoryWrite = $filesystem - ->getDirectoryWrite(DirectoryList::ROOT); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); + $this->directoryWrite = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $this->writeFactory = $this->objectManager->get(WriteFactory::class); + $this->csvFactory = $this->objectManager->get(CsvFactory::class); } /** @@ -142,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(), @@ -149,6 +169,93 @@ public function testImportData() ); } + /** + * Decompresses if gz compressed, stores in memory or temp file, and loads CSV adapter + * + * @param string $importData + * @return Import\AbstractSource + */ + private function createImportAdapter(string $importData) + { + if (0 === strncmp("\x1f\x8b", $importData, 2)) { // gz's magic string + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $importData = gzdecode($importData); + } + $openedFile = $this->writeFactory->create('php://temp', '', 'w'); + $openedFile->write($importData); + unset($importData); + $directory = $this->directoryWrite; + $adapter = $this->csvFactory->create(['directory' => $directory, 'file' => $openedFile]); + return $adapter; + } + + /** + * Test validateSource() and importData() and using same $ids between them + * + * @magentoDataFixture Magento/Customer/_files/import_export/customer.php + */ + public function testValidateSourceAndImportSource() + { + /** @var Import $import */ + $import = $this->objectManager->create(Import::class); + $importData = \file_get_contents(__DIR__ . '/_files/2k_customers.csv.gz'); + $source = $this->createImportAdapter($importData); + unset($importData); + $import->setData([ + 'form_key' => 'Ded3z8XBEaMWt3sH', + 'entity' => 'customer', + 'behavior' => 'add_update', + 'validation_strategy' => 'validation-stop-on-errors', + 'allowed_error_count' => '10', + '_import_field_separator' => ',', + '_import_multiple_value_separator' => ',', + '_import_empty_attribute_value_constant' => '__EMPTY__VALUE__', + 'import_images_file_dir' => '', + '_import_ids' => '', + ]); + $import->validateSource($source); + $ids = $import->getValidatedIds(); + $errorAggregator = $import->getErrorAggregator(); + $errorStrings = []; + foreach ($errorAggregator->getAllErrors() as $error) { + $errorStrings[] = sprintf( + "Error:\nRowNumber: %s\nColumnName: %s\nCode: %s\nDescription: %s\nLevel: %s\nMessage: %s\n", + (string)$error->getRowNumber(), + $error->getColumnName(), + $error->getErrorCode(), + $error->getErrorDescription(), + $error->getErrorLevel(), + $error->getErrorMessage(), + ); + } + if (!empty($errorStrings)) { + $exceptionString = sprintf( + "Errors:\n%s\n", + implode("\n", $errorStrings) + ); + throw new \Exception($exceptionString); + } + $this->assertCount(20, $ids); + /** @var Import $import2 */ + $import2 = $this->objectManager->create(Import::class); + $import2->setData([ + 'form_key' => 'DedGz8CNEaMWt3sH', + 'entity' => 'customer', + 'behavior' => 'add_update', + 'validation_strategy' => 'validation-stop-on-errors', + 'allowed_error_count' => '10', + '_import_field_separator' => ',', + '_import_multiple_value_separator' => ',', + '_import_empty_attribute_value_constant' => '__EMPTY__VALUE__', + 'import_images_file_dir' => '', + '_import_ids' => implode(',', $ids), + ]); + $this->assertEmpty($import2->getValidatedIds()); + $import2->importSource(); + $createdItemsCount = $import2->getCreatedItemsCount(); + $this->assertEquals(2000, $createdItemsCount); + } + /** * Tests importData() method. * diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/2k_customers.csv.gz b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/2k_customers.csv.gz new file mode 100644 index 000000000000..40479fb507cd Binary files /dev/null and b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/2k_customers.csv.gz differ 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 96c14c67607a..62cf355721ab 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 174d31d641fe..508eb6113fa2 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 7a7fcfc558d0..41414c2b5f9f 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 8cdeaa601811..9a0d6a4fd46d 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 1b298c57c25e..4055e11089ce 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 8651f2cc760d..e44cec9027e2 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 000000000000..14b8e35fce1f --- /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 f5a89a555866..90e06e577f8b 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 2f1b0a43a6c5..97ec9f459d3e 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 59359534d524..000000000000 --- 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 3f4fc72e4258..a5728a58a530 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 000000000000..fdf7776d7a84 --- /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 6d5f760d7894..88a104e6e29e 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 000000000000..4f3075decc27 --- /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 0c83a0bc6d60..c5cfa5c37307 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 000000000000..e6e588329356 --- /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 000000000000..4ab973e33c1c --- /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 000000000000..14756bf193c8 --- /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 000000000000..bf7b485ebfcf --- /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 7bd4b3a99d1b..470e434542ec 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 93ed3d84c845..72ba459997c3 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 f25880e10c81..9d48de03c736 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 000000000000..c113b1e2f9b6 --- /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 42f766c786c0..ab8588f229bb 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 1655dca029c1..91f47b1f4939 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 96e31a753ada..ec41e3b159e7 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 a6579998c7b9..2a65949db95e 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 9b5ea2f361ba..03de1a5ac09a 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 000000000000..570b051686ee --- /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 4a6c542a110b..df400561e2ff 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 98e2777467e8..d296661227b1 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 ad4491b166cf..859a912bc4f2 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 000000000000..1f3ea9076d24 --- /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 000000000000..9ed2557115a8 --- /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 000000000000..52203727c170 --- /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 000000000000..0d8d4651a5a2 --- /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 000000000000..6f5b9f5f8bba --- /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 000000000000..72054219a8ea --- /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 000000000000..52446dee26b1 --- /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 000000000000..f00790bddbf9 --- /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 8570242c6584..beed18868e76 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 2a34e1bfc07f..eadb68d67fbb 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 000000000000..ba26372132e6 --- /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 000000000000..13021cc9dc09 --- /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 000000000000..b9e61ad57de9 --- /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 000000000000..f8f8d3bd013a --- /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 000000000000..0eaf2a0735e9 --- /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 000000000000..60b0fc93455d --- /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 000000000000..eb9a30030cb9 --- /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/GroupedImportExport/Model/Import/Product/Type/GroupedTest.php b/dev/tests/integration/testsuite/Magento/GroupedImportExport/Model/Import/Product/Type/GroupedTest.php index e4c8feef2fd8..847caa06b2f4 100644 --- a/dev/tests/integration/testsuite/Magento/GroupedImportExport/Model/Import/Product/Type/GroupedTest.php +++ b/dev/tests/integration/testsuite/Magento/GroupedImportExport/Model/Import/Product/Type/GroupedTest.php @@ -24,22 +24,20 @@ use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class GroupedTest extends TestCase { /** * Configurable product test Name */ - const TEST_PRODUCT_NAME = 'Test Grouped'; + public const TEST_PRODUCT_NAME = 'Test Grouped'; /** * Configurable product test Type */ - const TEST_PRODUCT_TYPE = 'grouped'; - - /** - * @var ProductImport - */ - private $model; + public const TEST_PRODUCT_TYPE = 'grouped'; /** * @var ObjectManagerInterface @@ -59,7 +57,6 @@ class GroupedTest extends TestCase protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); - $this->model = $this->objectManager->create(ProductImport::class); } /** @@ -132,7 +129,6 @@ private function getStockItem(int $productId): ?StockItemInterface return reset($items); } - /** * Perform products import. * @@ -141,19 +137,15 @@ private function getStockItem(int $productId): ?StockItemInterface */ private function import(string $pathToFile): void { + /** @var ProductImport $model */ + $model = $this->objectManager->create(ProductImport::class); $filesystem = $this->objectManager->create(Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create(Csv::class, ['file' => $pathToFile, 'directory' => $directory]); - $errors = $this->model->setSource( - $source - )->setParameters( - [ - 'behavior' => Import::BEHAVIOR_APPEND, - 'entity' => 'catalog_product', - ] - )->validateData(); - + $model->setSource($source); + $model->setParameters(['behavior' => Import::BEHAVIOR_APPEND, 'entity' => 'catalog_product',]); + $errors = $model->validateData(); $this->assertTrue($errors->getErrorsCount() == 0); - $this->model->importData(); + $model->importData(); } } diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/ExportTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/ExportTest.php index 50972ef0325a..73aa382baafb 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 17a826ceae27..94244ebac1fe 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 000000000000..ae4825a8ccc6 --- /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 2fabcbc09eb3..a70ab27b0af0 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 e561311fc4e7..85b0b53e6426 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 63670e9cb458..f0c5ce25911f 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 719d78b07ca3..34df1deb4ff3 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 5e6447410db5..36b0efcef0b5 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 bde9bbd9b4d2..3e65ddac46a4 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 000000000000..1b285306e097 --- /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 000000000000..cb64b08da526 --- /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 2d3ee063ab5a..fde490fdf9e0 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 ab4450cb4e25..8c1904acc39e 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 340fbafa9119..39977b26d8c4 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 13436974d630..4e740eef15bb 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 ee21150bd612..dd1b5dbc6dbf 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 27423c67ffe1..9b8bb6a5d315 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 2de06558ab66..7d76b6cf8524 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 cffdda80cc89..2997f795bb72 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 3c93e5598e68..e3f3a98a7331 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 0850781f4437..244306bc5c5a 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 68a087c63651..d087c31c1c26 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 000000000000..8952bde98e38 --- /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 000000000000..08c5d552844f --- /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 5c7aa99a2e91..718a5f67bb71 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 c5b1df2ecea5..c054ff055dc9 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 a5fa4e402197..a6f13c073393 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 000000000000..a840e9680935 --- /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 000000000000..2a9d1c4b6af9 --- /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 3551408dbe2f..e3a96a77186a 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 ccab0fc9f154..1299c5fca09f 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 661987ad2662..ed0f2a23816e 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 b430d576db3e..553911cbd891 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 3bb10238989b..791db5516c57 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 964a6248c1c1..7323a5cf3cc2 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 b2d3df227377..e5c424ebb9a6 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 764976455ed7..6e2bf44dd752 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 e651c24a246a..f8ca0b85843e 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 d3e5d2226a87..3196c34b5d5c 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 b8b4ee5de188..8fc63c702d24 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 000000000000..1c19a80e7f35 --- /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 d81a6fa52ea4..bd83561a8a32 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 2b53c7e4c23b..c129b629eaac 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 110c93b62033..6b9e234c63a9 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 eef8cf960944..c14ab540f5c9 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 a119b6259b5f..f3b43d9f2cca 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 604a444883a2..44633c95f6f9 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 77320b474435..3ba66146feda 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 000000000000..d467f2bcbaed 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 000000000000..a66eb60d9870 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 ce271e510209..7c9ef0ed5af8 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 000000000000..d9051d5a555f --- /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 03b6e1da659d..000000000000 --- 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 000000000000..88ef80245555 --- /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 8caf02a5160a..000000000000 --- 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 000000000000..fdbb8bf43920 --- /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 9bc57aa289d7..000000000000 --- 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 000000000000..371c1aa4d194 --- /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 658bf756aacf..000000000000 --- 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 000000000000..a2f492521d20 --- /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 88fe2de81a3d..000000000000 --- 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 000000000000..5c24f32ade2d --- /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 1732594c57ea..000000000000 --- 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 000000000000..dac4a95f4521 --- /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 8de6b4598276..000000000000 --- 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 000000000000..550084b0b4c7 --- /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 7b8b3a906781..000000000000 --- 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 97a19e5086d7..000000000000 --- 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 e84e3aa7aefb..000000000000 --- 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 b5711f9f12bf..000000000000 --- 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 6d635d01b2f4..87709053c81b 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 000000000000..7cd37ec3ee49 --- /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 000000000000..0dfbc6528fcd --- /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 732d412b8396..652cabc2db8c 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 3d8325a3ecd5..2bd6fb20dbf8 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 000000000000..bc2b7d3cda6d --- /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 000000000000..3ed283973624 --- /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 f6f4927aaeda..12870be3a727 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 000000000000..c82aeb31d094 --- /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 22465b4e5da8..b10ac1c433dd 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 0a37db81e009..7df590d0d713 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/Payment/base/js/model/credit-card-validation/validator.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Payment/base/js/model/credit-card-validation/validator.test.js index c2907169273a..4e387e9d0ac9 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Payment/base/js/model/credit-card-validation/validator.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Payment/base/js/model/credit-card-validation/validator.test.js @@ -20,5 +20,14 @@ define([ expect($.validator.methods['validate-card-year']((year - 1).toString())).toBeFalsy(); expect($.validator.methods['validate-card-year']((year + 1).toString())).toBeTruthy(); }); + + it('Check credit card type validator.', function () { + var typeValidator = $.validator.methods['validate-card-type']; + + expect(typeValidator('4111111111111111', null, [{type: 'Visa'}])).toBeTruthy(); + expect(typeValidator('1111111111111111', null, [{type: 'Visa'}])).toBeFalsy(); + expect(typeValidator('6759411100000008', null, [{type: 'Maestro Domestic'}])).toBeTruthy(); + expect(typeValidator('4111676770111115', null, [{type: 'Maestro Domestic'}])).toBeFalsy(); + }); }); }); 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 e4a2b95a4c97..fed990964659 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 ba5ad61cfe31..d7516c64fedc 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 7a6da6dcae57..f95a5397b116 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 2e89ea208762..e094d37402f8 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 5db506b00a88..81709c949dee 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/js/jasmine/tests/lib/mage/validation.test.js b/dev/tests/js/jasmine/tests/lib/mage/validation.test.js index ca9ec877013f..6add659de34c 100644 --- a/dev/tests/js/jasmine/tests/lib/mage/validation.test.js +++ b/dev/tests/js/jasmine/tests/lib/mage/validation.test.js @@ -130,6 +130,12 @@ define([ expect($.validator.methods['validate-cc-type-select'].call( this, 'UN', null, $('')) ).toEqual(false); + expect($.validator.methods['validate-cc-type-select'].call( + this, 'MD', null, $('')) + ).toEqual(true); + expect($.validator.methods['validate-cc-type-select'].call( + this, 'MD', null, $('')) + ).toEqual(false); }); }); @@ -899,6 +905,7 @@ define([ '' + ''); @@ -938,6 +945,12 @@ define([ .call($.validator.prototype, '3528000000000007', null, select)).toEqual(true); expect($.validator.methods['validate-cc-type'] .call($.validator.prototype, '3095434000000001', null, select)).toEqual(false); + + select.val('MD'); + expect($.validator.methods['validate-cc-type'] + .call($.validator.prototype, '6759411100000008', null, select)).toEqual(true); + expect($.validator.methods['validate-cc-type'] + .call($.validator.prototype, '4111676770111115', null, select)).toEqual(false); }); }); 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 000000000000..f00c9a1eb08c --- /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 000000000000..5150187caf4f --- /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 000000000000..b6d4eca91d6a --- /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 000000000000..65a70da8d660 --- /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 000000000000..45cb5f6938b4 --- /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 000000000000..a6071bc64759 --- /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 000000000000..c4c9f12fbaee --- /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 000000000000..57b70edd9e3f --- /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 000000000000..bc469f23f6e2 --- /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 000000000000..86f8534abb7d --- /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 000000000000..403957ca0921 --- /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 000000000000..9ca6fcbc2275 --- /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 000000000000..3c62923cf256 --- /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 000000000000..cb8f53d499a5 --- /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 000000000000..6568a59de2a3 --- /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 000000000000..cb8f53d499a5 --- /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 000000000000..4d4922107431 --- /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 000000000000..8d164bdc311c --- /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 000000000000..dbf9f9019ca8 --- /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 000000000000..6834a540e7d6 --- /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 000000000000..5aef6dfc09f9 --- /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 000000000000..2ec165aedf08 --- /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 000000000000..c5266ca453ec --- /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 000000000000..1a1b02dce67c --- /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 000000000000..2507abeef299 --- /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 000000000000..bc469f23f6e2 --- /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 000000000000..86f8534abb7d --- /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 000000000000..403957ca0921 --- /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 000000000000..9ca6fcbc2275 --- /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 000000000000..b312fbfa758e --- /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 166a46970f84..d58f1c06a8a0 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 59226d02243d..3555fef7ddf6 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 0d9a282511c6..10dfa49484d9 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 d16854a6eae8..08c57690b83b 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 000000000000..886fbb2aaa21 --- /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 000000000000..b312fbfa758e --- /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 000000000000..df6006155f32 --- /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 000000000000..caed64721a25 --- /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 dc27b019f4c5..3de9a61a99c6 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 a79b2266bd92..cc1e4ae3a8da 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 6fe05ff189f7..f677ceee4a0e 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 e8c71332f35b..62036290187d 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 cb9f9bbadeea..8937fcda2090 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 8d2d631334a9..660bf5cbce69 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 0ecaf496dad7..45852174d4e3 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 7048575c3090..81ee07df3a21 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 e99ce8fb84ad..df09ad58276f 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 d3527960e6a1..6fa4da23663f 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 80fe4ec247a6..08ba4bba28c6 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 0e3b5fa3d341..f0934f580796 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 000000000000..b312fbfa758e --- /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 000000000000..f0309e8145d2 --- /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 473708d3301b..9ae376b9e21b 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 03adaca0589c..6e380c90f443 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 dbb979d22e63..ecf0133d4731 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 672ba683b198..41c002baf502 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 fa0d9072c498..b57642169c69 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 c3d565bc9964..f66d8ec9acbe 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 954c4f1e9a2c..e0d80799a10c 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 01b1ba5457a3..562fde6cab92 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 fb0533bdf9c4..463217292363 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 6d3fbcaca9db..d4acb4c8406e 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 896812f38c28..e5cf9d7c5488 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 5c95f83cb4a0..e2384a1c5608 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 8519b3e6d771..4a4f84df4caa 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 c81bc48ace4d..5f043fbf771a 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 3d6c2756595a..b028b71c299c 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 ea8f96e0c046..58a690a230b5 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 8c3cc1d55191..c69bf42bdc06 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 000000000000..c1a0412c805a --- /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 000000000000..137358f732b5 --- /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 000000000000..94626a587418 --- /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 000000000000..3d1621927091 --- /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 000000000000..61ad16f8969f --- /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 000000000000..e7475ef8b089 --- /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 000000000000..94754ae9f493 --- /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 4f6964d150fe..43bbcbf8deec 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 9ba4b269d21a..10504660f4cc 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 5d8d50bcb909..32a1cd4948d6 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 538e65354955..22a1af5a2048 100644 --- a/lib/internal/Magento/Framework/App/DeploymentConfig.php +++ b/lib/internal/Magento/Framework/App/DeploymentConfig.php @@ -35,14 +35,14 @@ class DeploymentConfig * * @var array */ - private $data; + private $data = []; /** * Flattened data * * @var array */ - private $flatData; + private $flatData = []; /** * Injected configuration data @@ -51,6 +51,21 @@ class DeploymentConfig */ private $overrideData; + /** + * @var array + */ + private $envOverrides = []; + + /** + * @var array + */ + private $readerLoad = []; + + /** + * @var array + */ + private $noConfigData = []; + /** * Constructor * @@ -76,16 +91,28 @@ public function __construct(DeploymentConfig\Reader $reader, $overrideData = []) */ public function get($key = null, $defaultValue = null) { - $this->load(); if ($key === null) { + if (empty($this->flatData)) { + $this->reloadData(); + } return $this->flatData; } + $result = $this->getByKey($key); + if ($result === null) { + if (empty($this->flatData) + || !isset($this->flatData[$key]) && !isset($this->noConfigData[$key]) + || count($this->getAllEnvOverrides()) + ) { + $this->resetData(); + $this->reloadData(); + } - if (array_key_exists($key, $this->flatData) && $this->flatData[$key] === null) { - return ''; + if (!isset($this->flatData[$key])) { + $this->noConfigData[$key] = $key; + } + $result = $this->getByKey($key); } - - return $this->flatData[$key] ?? $defaultValue; + return $result ?? $defaultValue; } /** @@ -97,27 +124,31 @@ public function get($key = null, $defaultValue = null) */ public function isAvailable() { - $this->load(); - return isset($this->flatData[ConfigOptionsListConstants::CONFIG_PATH_INSTALL_DATE]); + return $this->get(ConfigOptionsListConstants::CONFIG_PATH_INSTALL_DATE) !== null; } /** * Gets a value specified key from config data * - * @param string $key + * @param string|null $key * @return null|mixed * @throws FileSystemException * @throws RuntimeException */ public function getConfigData($key = null) { - $this->load(); - - if ($key !== null && !isset($this->data[$key])) { - return null; + if ($key === null) { + if (empty($this->data)) { + $this->reloadInitialData(); + } + return $this->data; } - - return $this->data[$key] ?? $this->data; + $result = $this->getConfigDataByKey($key); + if ($result === null) { + $this->reloadInitialData(); + $result = $this->getConfigDataByKey($key); + } + return $result; } /** @@ -127,7 +158,8 @@ public function getConfigData($key = null) */ public function resetData() { - $this->data = null; + $this->data = []; + $this->flatData = []; } /** @@ -140,8 +172,7 @@ public function resetData() */ public function isDbAvailable() { - $this->load(); - return isset($this->data['db']); + return $this->getConfigData('db') !== null; } /** @@ -164,17 +195,41 @@ private function getEnvOverride() : array * @throws FileSystemException * @throws RuntimeException */ - private function load() + private function reloadInitialData(): void + { + if (empty($this->readerLoad) || empty($this->data) || empty($this->flatData)) { + $this->readerLoad = $this->reader->load(); + } + $this->data = array_replace( + $this->readerLoad, + $this->overrideData ?? [], + $this->getEnvOverride() + ); + } + + /** + * Loads the configuration data + * + * @return void + * @throws FileSystemException + * @throws RuntimeException + */ + private function reloadData(): void { - if (empty($this->data)) { - $this->data = array_replace( - $this->reader->load(), - $this->overrideData ?? [], - $this->getEnvOverride() - ); - // flatten data for config retrieval using get() - $this->flatData = $this->flattenParams($this->data); + $this->reloadInitialData(); + // flatten data for config retrieval using get() + $this->flatData = $this->flattenParams($this->data); + $this->flatData = $this->getAllEnvOverrides() + $this->flatData; + } + /** + * 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 @@ -184,10 +239,11 @@ private function load() ) { // 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; + $this->envOverrides[$flatKey] = $value; } } } + return $this->envOverrides; } /** @@ -197,12 +253,12 @@ private function load() * each level of array is accessible by path key * * @param array $params - * @param string $path - * @param array $flattenResult + * @param string|null $path + * @param array|null $flattenResult * @return array * @throws RuntimeException */ - private function flattenParams(array $params, $path = null, array &$flattenResult = null) : array + private function flattenParams(array $params, ?string $path = null, array &$flattenResult = null): array { if (null === $flattenResult) { $flattenResult = []; @@ -236,4 +292,41 @@ private function flattenParams(array $params, $path = null, array &$flattenResul return $flattenResult; } + + /** + * Returns flat data by key + * + * @param string|null $key + * @return mixed|null + */ + private function getByKey(?string $key) + { + if (array_key_exists($key, $this->flatData) && $this->flatData[$key] === null) { + return ''; + } + + return $this->flatData[$key] ?? null; + } + + /** + * Returns data by key + * + * @param string|null $key + * @return mixed|null + */ + 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 b3fa5a5cca67..6c4648be087f 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 1d73bdf4a995..9b4d42e9335f 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 10901e418963..b0ef3345f98a 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 000000000000..e3b92b241aae --- /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/ProductMetadata.php b/lib/internal/Magento/Framework/App/ProductMetadata.php index 55e98bb085d4..e522d7b1b3ed 100644 --- a/lib/internal/Magento/Framework/App/ProductMetadata.php +++ b/lib/internal/Magento/Framework/App/ProductMetadata.php @@ -25,7 +25,7 @@ class ProductMetadata implements ProductMetadataInterface /** * Magento product name */ - const PRODUCT_NAME = 'Magento'; + const PRODUCT_NAME = 'Mage-OS'; /** * Magento version cache key diff --git a/lib/internal/Magento/Framework/App/README.md b/lib/internal/Magento/Framework/App/README.md index 87665be48425..99736f9f8018 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 000000000000..5c7e75edaf23 --- /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 000000000000..af3d697a8fb9 --- /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 000000000000..7620c94daf46 --- /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 000000000000..443628b6a0b5 --- /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 c03b56c7eacb..2535b499f8a6 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 f572533ff8db..1c57c42f766f 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 000000000000..f4c9b8486b06 --- /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 cb36408e9c92..feabc97705bd 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 02ef8e8123d0..232120ff75dd 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 62839e634983..0235f0b43294 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 bc2b85b37442..5956c7063c89 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 000000000000..e2cfc00e6d49 --- /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 000000000000..30eace104f6c --- /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 f87fe9f672a2..36eb1901e5b3 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 6f5706cd94a7..a3a01040628e 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 03879adad7cd..759f54c93006 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 191a86c442f9..5d586fc6686e 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfigTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfigTest.php @@ -1,8 +1,10 @@ 'scalar_value', - 'configData2' => [ + 'configData1' => 'scalar_value', + 'configData2' => [ 'foo' => 1, 'bar' => ['baz' => 2], ], - 'configData3' => null, + 'configData3' => null, 'test_override' => 'original', ]; @@ -34,16 +38,24 @@ class DeploymentConfigTest extends TestCase */ private static $flattenedFixture = [ - 'configData1' => 'scalar_value', - 'configData2' => [ + 'configData1' => 'scalar_value', + 'configData2' => [ 'foo' => 1, 'bar' => ['baz' => 2], ], - 'configData2/foo' => 1, - 'configData2/bar' => ['baz' => 2], + 'configData2/foo' => 1, + 'configData2/bar' => ['baz' => 2], 'configData2/bar/baz' => 2, - 'configData3' => null, - 'test_override' => 'overridden', + 'configData3' => null, + 'test_override' => 'overridden', + ]; + + /** + * @var array + */ + private static $flattenedFixtureSecond + = [ + 'test_override' => 'overridden2' ]; /** @@ -59,7 +71,7 @@ class DeploymentConfigTest extends TestCase /** * @var DeploymentConfig */ - protected $_deploymentConfig; + protected $deploymentConfig; /** * @var DeploymentConfig @@ -69,81 +81,119 @@ class DeploymentConfigTest extends TestCase /** * @var MockObject */ - private $reader; + private $readerMock; public static function setUpBeforeClass(): void { - self::$fixtureConfig = require __DIR__ . '/_files/config.php'; + self::$fixtureConfig = require __DIR__ . '/_files/config.php'; self::$fixtureConfigMerged = require __DIR__ . '/_files/other/local_developer_merged.php'; } protected function setUp(): void { - $this->reader = $this->createMock(Reader::class); - $this->_deploymentConfig = new DeploymentConfig( - $this->reader, + $this->readerMock = $this->createMock(Reader::class); + $this->deploymentConfig = new DeploymentConfig( + $this->readerMock, ['test_override' => 'overridden'] ); $this->_deploymentConfigMerged = new DeploymentConfig( - $this->reader, + $this->readerMock, require __DIR__ . '/_files/other/local_developer.php' ); } + /** + * @return void + * @throws FileSystemException + * @throws RuntimeException + */ public function testGetters(): void { - $this->reader->expects($this->once())->method('load')->willReturn(self::$fixture); - $this->assertSame(self::$flattenedFixture, $this->_deploymentConfig->get()); - // second time to ensure loader will be invoked only once - $this->assertSame(self::$flattenedFixture, $this->_deploymentConfig->get()); - $this->assertSame('scalar_value', $this->_deploymentConfig->getConfigData('configData1')); - $this->assertSame(self::$fixture['configData2'], $this->_deploymentConfig->getConfigData('configData2')); - $this->assertSame(self::$fixture['configData3'], $this->_deploymentConfig->getConfigData('configData3')); - $this->assertSame('', $this->_deploymentConfig->get('configData3')); - $this->assertSame('defaultValue', $this->_deploymentConfig->get('invalid_key', 'defaultValue')); - $this->assertNull($this->_deploymentConfig->getConfigData('invalid_key')); - $this->assertSame('overridden', $this->_deploymentConfig->get('test_override')); + $this->readerMock->expects($this->any())->method('load')->willReturn(self::$fixture); + $this->assertSame(self::$flattenedFixture, $this->deploymentConfig->get()); + $this->assertSame('scalar_value', $this->deploymentConfig->getConfigData('configData1')); + $this->assertSame(self::$fixture['configData2'], $this->deploymentConfig->getConfigData('configData2')); + $this->assertSame(self::$fixture['configData3'], $this->deploymentConfig->getConfigData('configData3')); + $this->assertSame('', $this->deploymentConfig->get('configData3')); + $this->assertSame('defaultValue', $this->deploymentConfig->get('invalid_key', 'defaultValue')); + $this->assertNull($this->deploymentConfig->getConfigData('invalid_key')); + $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 + * @throws RuntimeException + */ public function testIsAvailable(): void { - $this->reader->expects($this->once())->method('load')->willReturn( + $this->readerMock->expects($this->once())->method('load')->willReturn( [ ConfigOptionsListConstants::CONFIG_PATH_INSTALL_DATE => 1, ] ); - $object = new DeploymentConfig($this->reader); + $object = new DeploymentConfig($this->readerMock); $this->assertTrue($object->isAvailable()); } + /** + * @return void + * @throws FileSystemException + * @throws RuntimeException + */ public function testNotAvailable(): void { - $this->reader->expects($this->once())->method('load')->willReturn([]); - $object = new DeploymentConfig($this->reader); + $this->readerMock->expects($this->once())->method('load')->willReturn([]); + $object = new DeploymentConfig($this->readerMock); $this->assertFalse($object->isAvailable()); } /** * test if the configuration changes during the same request, the configuration remain the same + * + * @return void + * @throws FileSystemException + * @throws RuntimeException */ public function testNotAvailableThenAvailable(): void { - $this->reader->expects($this->once())->method('load')->willReturn(['Test']); - $object = new DeploymentConfig($this->reader); + $this->readerMock->expects($this->exactly(1))->method('load')->willReturn(['Test']); + $object = new DeploymentConfig($this->readerMock); $this->assertFalse($object->isAvailable()); $this->assertFalse($object->isAvailable()); } /** - * @param array $data * @dataProvider keyCollisionDataProvider + * @param array $data + * @throws FileSystemException + * @throws RuntimeException */ public function testKeyCollision(array $data): void { $this->expectException('Exception'); $this->expectExceptionMessage('Key collision'); - $this->reader->expects($this->once())->method('load')->willReturn($data); - $object = new DeploymentConfig($this->reader); + $this->readerMock->expects($this->once())->method('load')->willReturn($data); + $object = new DeploymentConfig($this->readerMock); $object->get(); } @@ -171,49 +221,71 @@ public function keyCollisionDataProvider(): array ]; } + /** + * @return void + * @throws FileSystemException + * @throws RuntimeException + */ public function testResetData(): void { - $this->reader->expects($this->exactly(2))->method('load')->willReturn(self::$fixture); - $this->assertSame(self::$flattenedFixture, $this->_deploymentConfig->get()); - $this->_deploymentConfig->resetData(); + $this->readerMock->expects($this->exactly(2))->method('load')->willReturn(self::$fixture); + $this->assertSame(self::$flattenedFixture, $this->deploymentConfig->get()); + $this->deploymentConfig->resetData(); // second time to ensure loader will be invoked only once after reset - $this->assertSame(self::$flattenedFixture, $this->_deploymentConfig->get()); - $this->assertSame(self::$flattenedFixture, $this->_deploymentConfig->get()); + $this->assertSame(self::$flattenedFixture, $this->deploymentConfig->get()); + $this->assertSame(self::$flattenedFixture, $this->deploymentConfig->get()); } + /** + * @return void + * @throws FileSystemException + * @throws RuntimeException + */ public function testIsDbAvailable(): void { - $this->reader->expects($this->exactly(2))->method('load')->willReturnOnConsecutiveCalls([], ['db' => []]); - $this->assertFalse($this->_deploymentConfig->isDbAvailable()); - $this->_deploymentConfig->resetData(); - $this->assertTrue($this->_deploymentConfig->isDbAvailable()); + $this->readerMock->expects($this->exactly(2))->method('load')->willReturnOnConsecutiveCalls([], ['db' => []]); + $this->assertFalse($this->deploymentConfig->isDbAvailable()); + $this->assertTrue($this->deploymentConfig->isDbAvailable()); + } + + /** + * @return void + * @throws FileSystemException + * @throws RuntimeException + */ + public function testResetDataOnMissingConfig(): void + { + $this->readerMock->expects($this->once())->method('load')->willReturn(self::$fixture); + $defaultValue = 'some_default_value'; + $result = $this->deploymentConfig->get('missing/key', $defaultValue); + $this->assertEquals($defaultValue, $result); } - public function testNoEnvVariables() + public function testNoEnvVariables(): void { - $this->reader->expects($this->once())->method('load')->willReturn(['a'=>'b']); - $this->assertSame('b', $this->_deploymentConfig->get('a')); + $this->readerMock->expects($this->once())->method('load')->willReturn(['a'=>'b']); + $this->assertSame('b', $this->deploymentConfig->get('a')); } - public function testEnvVariables() + public function testEnvVariables(): void { - $this->reader->expects($this->once())->method('load')->willReturn([]); + $this->readerMock->expects($this->once())->method('load')->willReturn([]); putenv('MAGENTO_DC__OVERRIDE={"a": "c"}'); - $this->assertSame('c', $this->_deploymentConfig->get('a')); + $this->assertSame('c', $this->deploymentConfig->get('a')); } - public function testEnvVariablesWithNoBaseConfig() + public function testEnvVariablesWithNoBaseConfig(): void { - $this->reader->expects($this->once())->method('load')->willReturn(['a'=>'b']); + $this->readerMock->expects($this->once())->method('load')->willReturn(['a'=>'b']); putenv('MAGENTO_DC_A=c'); putenv('MAGENTO_DC_B__B__B=D'); - $this->assertSame('c', $this->_deploymentConfig->get('a')); - $this->assertSame('D', $this->_deploymentConfig->get('b/b/b')); + $this->assertSame('c', $this->deploymentConfig->get('a')); + $this->assertSame('D', $this->deploymentConfig->get('b/b/b')); } - public function testEnvVariablesSubstitution() + public function testEnvVariablesSubstitution(): void { - $this->reader->expects($this->once()) + $this->readerMock->expects($this->once()) ->method('load') ->willReturn( [ @@ -224,8 +296,38 @@ public function testEnvVariablesSubstitution() ); putenv('MAGENTO_DC____A=c'); putenv('MAGENTO_DC____B=D'); - $this->assertSame('c', $this->_deploymentConfig->get('a')); - $this->assertSame('D', $this->_deploymentConfig->get('b'), 'return value from env'); - $this->assertSame('e$%^&', $this->_deploymentConfig->get('c'), 'return default value'); + $this->assertSame('c', $this->deploymentConfig->get('a')); + $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 164286728f3b..ea6b4cbf9b19 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 000000000000..7e95505efae9 --- /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 d23dd79cd71a..13c15e2869fe 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 000000000000..0fdd80b79567 --- /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 7fbbedd7f913..5036ac399296 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 5f4af5e8ae51..36f1ddaf5beb 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 68714923429c..69e7e19f2a45 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 7b27f84a9efd..49186d22542d 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 4fa38a201db6..197089214b0a 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 f71598637601..ac4b9772a562 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 b58ad53dd139..1409fba14c5f 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 e3785a3025e3..4cddf00d77b9 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 8ddbc686147f..d0ee4069093f 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 565777d68ff6..c02878ef7950 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/Backend/RemoteSynchronizedCache.php b/lib/internal/Magento/Framework/Cache/Backend/RemoteSynchronizedCache.php index 71f67b4aa603..04efd1c60c4c 100644 --- a/lib/internal/Magento/Framework/Cache/Backend/RemoteSynchronizedCache.php +++ b/lib/internal/Magento/Framework/Cache/Backend/RemoteSynchronizedCache.php @@ -195,14 +195,14 @@ public function load($id, $doNotTestCacheValidity = false) { $localData = $this->local->load($id); - if ($localData) { + if ($localData !== false) { if ($this->getDataVersion($localData) === $this->loadRemoteDataVersion($id)) { return $localData; } } $remoteData = $this->remote->load($id); - if ($remoteData) { + if ($remoteData !== false) { $this->local->save($remoteData, $id); return $remoteData; @@ -233,10 +233,15 @@ public function save($data, $id, $tags = [], $specificLifetime = false) { $dataToSave = $data; $remHash = $this->loadRemoteDataVersion($id); - + $isRemoteUpToDate = false; if ($remHash !== false && $this->getDataVersion($data) === $remHash) { - $dataToSave = $this->remote->load($id); - } else { + $remoteData = $this->remote->load($id); + if ($remoteData !== false && $this->getDataVersion($data) === $this->getDataVersion($remoteData)) { + $isRemoteUpToDate = true; + $dataToSave = $remoteData; + } + } + if (!$isRemoteUpToDate) { $this->remote->save($data, $id, $tags, $specificLifetime); $this->saveRemoteDataVersion($data, $id, $tags, $specificLifetime); } diff --git a/lib/internal/Magento/Framework/Cache/Core.php b/lib/internal/Magento/Framework/Cache/Core.php index 1c1bab29b75a..2a97bc0f49c7 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 43d261c1ed07..f9e6ccdeb172 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 785ca43ec935..737105585e2e 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/Backend/RemoteSynchronizedCacheTest.php b/lib/internal/Magento/Framework/Cache/Test/Unit/Backend/RemoteSynchronizedCacheTest.php index d4a83180796e..4b3b57a77b82 100644 --- a/lib/internal/Magento/Framework/Cache/Test/Unit/Backend/RemoteSynchronizedCacheTest.php +++ b/lib/internal/Magento/Framework/Cache/Test/Unit/Backend/RemoteSynchronizedCacheTest.php @@ -10,17 +10,11 @@ use Magento\Framework\Cache\Backend\Database; use Magento\Framework\Cache\Backend\RemoteSynchronizedCache; use Magento\Framework\DB\Adapter\Pdo\Mysql; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class RemoteSynchronizedCacheTest extends TestCase { - /** - * @var ObjectManager - */ - protected $objectManager; - /** * @var \Cm_Cache_Backend_File|MockObject */ @@ -41,24 +35,12 @@ class RemoteSynchronizedCacheTest extends TestCase */ protected function setUp(): void { - $this->objectManager = new ObjectManager($this); - - $this->localCacheMockExample = $this->getMockBuilder(\Cm_Cache_Backend_File::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->remoteCacheMockExample = $this->getMockBuilder(Database::class) - ->disableOriginalConstructor() - ->getMock(); - /** @var \Magento\Framework\Cache\Backend\Database $databaseCacheInstance */ - - $this->remoteSyncCacheInstance = $this->objectManager->getObject( - RemoteSynchronizedCache::class, + $this->localCacheMockExample = $this->createMock(\Cm_Cache_Backend_File::class); + $this->remoteCacheMockExample = $this->createMock(Database::class); + $this->remoteSyncCacheInstance = new RemoteSynchronizedCache( [ - 'options' => [ - 'remote_backend' => $this->remoteCacheMockExample, - 'local_backend' => $this->localCacheMockExample - ] + 'remote_backend' => $this->remoteCacheMockExample, + 'local_backend' => $this->localCacheMockExample ] ); } @@ -67,19 +49,13 @@ protected function setUp(): void * Test that exception is thrown if cache is not configured. * * @param array $options - * * @return void * @dataProvider initializeWithExceptionDataProvider */ public function testInitializeWithException($options): void { $this->expectException('Zend_Cache_Exception'); - $this->objectManager->getObject( - RemoteSynchronizedCache::class, - [ - 'options' => $options - ] - ); + new RemoteSynchronizedCache($options); } /** @@ -119,12 +95,7 @@ public function initializeWithExceptionDataProvider(): array */ public function testInitializeWithOutException($options): void { - $result = $this->objectManager->getObject( - RemoteSynchronizedCache::class, - [ - 'options' => $options - ] - ); + $result = new RemoteSynchronizedCache($options); $this->assertInstanceOf(RemoteSynchronizedCache::class, $result); } @@ -377,6 +348,38 @@ public function testSaveWithEqualRemoteData(): void $this->remoteSyncCacheInstance->save($remoteData, 1, $tags); } + /** + * Test data save when remote data are missed but hash exists. + * + * @return void + */ + public function testSaveWithEqualHashesAndMissedRemoteData(): void + { + $cacheKey = 'key'; + $dataToSave = '2'; + $remoteData = '1'; + $tags = ['MAGE']; + + $this->remoteCacheMockExample + ->method('load') + ->willReturnOnConsecutiveCalls(\hash('sha256', $dataToSave), $remoteData); + + $this->remoteCacheMockExample + ->expects($this->exactly(2)) + ->method('save') + ->withConsecutive( + [$dataToSave, $cacheKey, $tags], + [\hash('sha256', $dataToSave), $cacheKey . ':hash', $tags] + )->willReturn(true); + $this->localCacheMockExample + ->expects($this->once()) + ->method('save') + ->with($dataToSave, $cacheKey, []) + ->willReturn(true); + + $this->remoteSyncCacheInstance->save($dataToSave, $cacheKey, $tags); + } + /** * @return void */ diff --git a/lib/internal/Magento/Framework/Cache/Test/Unit/CoreTest.php b/lib/internal/Magento/Framework/Cache/Test/Unit/CoreTest.php index 503fb1a569e2..deb7bd5ee348 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 6868b68e8d88..c05860792209 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 759168372fdc..9bc0551b4d1d 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 cf0dce084c5d..105b0a8afec7 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 6e99e472cb79..02df6996bc65 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 6084b574235e..9f402e079c45 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 1e4168ca5b62..d5c5b428a75f 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 670c74dd197b..667d78ca7d36 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 cc11b32c410b..a847b7f45e2b 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 e453e8397a9a..ad0daf53198c 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 a5cdbf72aa1e..dfcc8530a8ab 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 b05269b33689..061d0a825acc 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 21bf423ff87b..3d7f01ca3133 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 245b85a27d4e..534436c2dc69 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 ec10676d5262..bf1649b0f4c2 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 e201978a8cb9..1f15340d1b9b 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 ac7b89ec8f09..4d7e59fde800 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 7b551268d899..41cd2eb7c3cf 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 bfbf194715dc..76e50b89da61 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 2a7b46812470..d7ccad3a7d70 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 ce1d54948520..3f421e4c729c 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 e04bbdc486e0..ed3d0949841d 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; } @@ -649,6 +701,7 @@ public function query($sql, $bind = []) * @throws LocalizedException In case multiple queries are attempted at once, to protect from SQL injection * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @deprecated 101.0.0 + * @see _query */ public function multiQuery($sql, $bind = []) { @@ -816,6 +869,7 @@ public function setQueryHook($hook) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @deprecated 100.1.2 + * @see MAGETWO-60073 */ protected function _splitMultiQuery($sql) { @@ -1516,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. @@ -1715,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. * @@ -1994,7 +2069,11 @@ public function insertOnDuplicate($table, array $data, array $fields = []) if (array_diff($cols, array_keys($row))) { throw new \Zend_Db_Exception('Invalid data for insert'); } - $values[] = $this->_prepareInsertData($row, $bind); + $line = []; + foreach ($cols as $field) { + $line[] = $row[$field]; + } + $values[] = $this->_prepareInsertData($line, $bind); } unset($row); } else { // Column-value pairs @@ -2233,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); } @@ -3621,6 +3704,7 @@ private function renderOnDuplicate($table, array $fields) * @return \Magento\Framework\DB\Select[] * @throws LocalizedException * @deprecated 100.1.3 + * @see MAGETWO-55589 */ public function selectsByRange($rangeField, \Magento\Framework\DB\Select $select, $stepCount = 100) { @@ -3637,6 +3721,7 @@ public function selectsByRange($rangeField, \Magento\Framework\DB\Select $select * * @return QueryGenerator * @deprecated 100.1.3 + * @see MAGETWO-55589 */ private function getQueryGenerator() { @@ -4136,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 def51db16454..46025f400b1d 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 0bd582b53477..4f904e73a188 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 9a417b4f837a..cbaa573aba8b 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 b829f063ac2d..4ce4156e72fd 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 34d065c42c9d..3fdd38dc0024 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 ba0368ef298a..0cd6bfb1e710 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 8be40b41fd73..f0cd404637d3 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 ce6639a98db2..366f4216100e 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 e1b45feb99a3..4a73ffd5014c 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 3232ef35dc80..6ff45a6ceada 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 222f9588a1cc..3e8ed3e62558 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 5847ab6eedd0..9aba8ea8ba19 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 = '