diff --git a/app/code/Magento/Braintree/etc/adminhtml/system.xml b/app/code/Magento/Braintree/etc/adminhtml/system.xml index 5f62a9d7cf1920ebdfc8020befa0f78f00c396bc..e4f4e11983892f98cfc8945ee2a541c428b441dd 100644 --- a/app/code/Magento/Braintree/etc/adminhtml/system.xml +++ b/app/code/Magento/Braintree/etc/adminhtml/system.xml @@ -33,7 +33,7 @@ </requires> </field> <field id="braintree_cc_vault_active" translate="label" type="select" sortOrder="12" showInDefault="1" showInWebsite="1" showInStore="0"> - <label>Vault enabled</label> + <label>Vault Enabled</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <config_path>payment/braintree_cc_vault/active</config_path> <requires> @@ -155,7 +155,7 @@ <comment>It is recommended to set this value to "PayPal" per store views.</comment> </field> <field id="braintree_paypal_vault_active" translate="label" type="select" sortOrder="21" showInDefault="1" showInWebsite="1" showInStore="0"> - <label>Vault enabled</label> + <label>Vault Enabled</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <config_path>payment/braintree_paypal_vault/active</config_path> <requires> diff --git a/app/code/Magento/Braintree/view/adminhtml/web/styles.css b/app/code/Magento/Braintree/view/adminhtml/web/styles.css index 31f48cd0b28df1d8235b9ccc9d6b46b13b410bd4..81378f636eb61362ad2840bc859161c91ee8add1 100644 --- a/app/code/Magento/Braintree/view/adminhtml/web/styles.css +++ b/app/code/Magento/Braintree/view/adminhtml/web/styles.css @@ -3,6 +3,6 @@ * See COPYING.txt for license details. */ -.braintree-section .heading {display: inline-block; background: url("images/braintree_logo.png") no-repeat 0 50% / 18rem auto; padding-left: 20rem;} -.braintree-section .button-container {display: inline-block; float: right;} +.braintree-section .heading {background: url("images/braintree_logo.png") no-repeat 0 50% / 18rem auto; padding-left: 20rem;} +.braintree-section .button-container {float: right;} .braintree-section .config-alt {background: url("images/braintree_allinone.png") no-repeat scroll 0 0 / 100% auto; height: 28px; margin: 0.5rem 0 0; width: 230px;} \ No newline at end of file diff --git a/app/code/Magento/Deploy/Console/Command/DeployStaticContentCommand.php b/app/code/Magento/Deploy/Console/Command/DeployStaticContentCommand.php index 9eda5f84e16b0e45d8621ce26d75c79f139f6192..51345c2b7609cb8e39cf25fb9a4f4f202cba6f65 100644 --- a/app/code/Magento/Deploy/Console/Command/DeployStaticContentCommand.php +++ b/app/code/Magento/Deploy/Console/Command/DeployStaticContentCommand.php @@ -15,12 +15,10 @@ use Symfony\Component\Console\Input\InputArgument; use Magento\Framework\App\ObjectManagerFactory; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Validator\Locale; -use Magento\Framework\Console\Cli; -use Magento\Deploy\Model\ProcessManager; -use Magento\Deploy\Model\Process; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\App\State; use Magento\Deploy\Console\Command\DeployStaticOptionsInterface as Options; +use Magento\Deploy\Model\DeployManager; /** * Deploy static content command @@ -121,109 +119,116 @@ class DeployStaticContentCommand extends Command Options::FORCE_RUN, '-f', InputOption::VALUE_NONE, - 'If specified, then run files will be deployed in any mode.' + 'Deploy files in any mode.' ), new InputOption( Options::NO_JAVASCRIPT, null, InputOption::VALUE_NONE, - 'If specified, no JavaScript will be deployed.' + 'Do not deploy JavaScript files' ), new InputOption( Options::NO_CSS, null, InputOption::VALUE_NONE, - 'If specified, no CSS will be deployed.' + 'Do not deploy CSS files.' ), new InputOption( Options::NO_LESS, null, InputOption::VALUE_NONE, - 'If specified, no LESS will be deployed.' + 'Do not deploy LESS files.' ), new InputOption( Options::NO_IMAGES, null, InputOption::VALUE_NONE, - 'If specified, no images will be deployed.' + 'Do not deploy images.' ), new InputOption( Options::NO_FONTS, null, InputOption::VALUE_NONE, - 'If specified, no font files will be deployed.' + 'Do not deploy font files.' ), new InputOption( Options::NO_HTML, null, InputOption::VALUE_NONE, - 'If specified, no html files will be deployed.' + 'Do not deploy HTML files.' ), new InputOption( Options::NO_MISC, null, InputOption::VALUE_NONE, - 'If specified, no miscellaneous files will be deployed.' + 'Do not deploy other types of files (.md, .jbf, .csv, etc...).' ), new InputOption( Options::NO_HTML_MINIFY, null, InputOption::VALUE_NONE, - 'If specified, html will not be minified.' + 'Do not minify HTML files.' ), new InputOption( Options::THEME, '-t', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, - 'If specified, just specific theme(s) will be actually deployed.', + 'Generate static view files for only the specified themes.', ['all'] ), new InputOption( Options::EXCLUDE_THEME, null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, - 'If specified, exclude specific theme(s) from deployment.', + 'Do not generate files for the specified themes.', ['none'] ), new InputOption( Options::LANGUAGE, '-l', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, - 'List of languages you want the tool populate files for.', + 'Generate files only for the specified languages.', ['all'] ), new InputOption( Options::EXCLUDE_LANGUAGE, null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, - 'List of langiages you do not want the tool populate files for.', + 'Do not generate files for the specified languages.', ['none'] ), new InputOption( Options::AREA, '-a', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, - 'List of areas you want the tool populate files for.', + 'Generate files only for the specified areas.', ['all'] ), new InputOption( Options::EXCLUDE_AREA, null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, - 'List of areas you do not want the tool populate files for.', + 'Do not generate files for the specified areas.', ['none'] ), new InputOption( Options::JOBS_AMOUNT, '-j', InputOption::VALUE_OPTIONAL, - 'Amount of jobs to which script can be paralleled.', + 'Enable parallel processing using the specified number of jobs.', self::DEFAULT_JOBS_AMOUNT ), + new InputOption( + Options::SYMLINK_LOCALE, + null, + InputOption::VALUE_NONE, + 'Create symlinks for the files of those locales, which are passed for deployment, ' + . 'but have no customizations' + ), new InputArgument( self::LANGUAGES_ARGUMENT, InputArgument::IS_ARRAY, - 'List of languages you want the tool populate files for.' + 'Space-separated list of ISO-636 language codes for which to output static view files.' ), ]); @@ -374,8 +379,11 @@ class DeployStaticContentCommand extends Command if (!$input->getOption(Options::FORCE_RUN) && $this->getAppState()->getMode() !== State::MODE_PRODUCTION) { throw new LocalizedException( __( - "Deploy static content is applicable only for production mode.\n" - . "Please use command 'bin/magento deploy:mode:set production' for set up production mode." + 'NOTE: Manual static content deployment is not required in "default" and "developer" modes.' + . PHP_EOL . 'In "default" and "developer" modes static contents are being deployed ' + . 'automatically on demand.' + . PHP_EOL . 'If you still want to deploy in these modes, use -f option: ' + . "'bin/magento setup:static-content:deploy -f'" ) ); } @@ -390,20 +398,24 @@ class DeployStaticContentCommand extends Command $output->writeln("Requested areas: " . implode(', ', array_keys($deployableAreaThemeMap))); $output->writeln("Requested themes: " . implode(', ', $requestedThemes)); - $deployer = $this->objectManager->create( - \Magento\Deploy\Model\Deployer::class, + /** @var $deployManager DeployManager */ + $deployManager = $this->objectManager->create( + DeployManager::class, [ - 'filesUtil' => $filesUtil, 'output' => $output, 'options' => $this->input->getOptions(), ] ); - if ($this->isCanBeParalleled()) { - return $this->runProcessesInParallel($deployer, $deployableAreaThemeMap, $deployableLanguages); - } else { - return $this->deploy($deployer, $deployableLanguages, $deployableAreaThemeMap); + foreach ($deployableAreaThemeMap as $area => $themes) { + foreach ($deployableLanguages as $locale) { + foreach ($themes as $themePath) { + $deployManager->addPack($area, $themePath, $locale); + } + } } + + return $deployManager->deploy(); } /** @@ -464,89 +476,4 @@ class DeployStaticContentCommand extends Command return [$deployableLanguages, $deployableAreaThemeMap, $requestedThemes]; } - - /** - * @param \Magento\Deploy\Model\Deployer $deployer - * @param array $deployableLanguages - * @param array $deployableAreaThemeMap - * @return int - */ - private function deploy($deployer, $deployableLanguages, $deployableAreaThemeMap) - { - return $deployer->deploy( - $this->objectManagerFactory, - $deployableLanguages, - $deployableAreaThemeMap - ); - } - - /** - * @param \Magento\Deploy\Model\Deployer $deployer - * @param array $deployableAreaThemeMap - * @param array $deployableLanguages - * @return int - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - private function runProcessesInParallel($deployer, $deployableAreaThemeMap, $deployableLanguages) - { - /** @var ProcessManager $processManager */ - $processManager = $this->objectManager->create(ProcessManager::class); - $processNumber = 0; - $processQueue = []; - foreach ($deployableAreaThemeMap as $area => &$themes) { - foreach ($themes as $theme) { - foreach ($deployableLanguages as $lang) { - $deployerFunc = function (Process $process) use ($area, $theme, $lang, $deployer) { - return $this->deploy($deployer, [$lang], [$area => [$theme]]); - }; - if ($processNumber >= $this->getProcessesAmount()) { - $processQueue[] = $deployerFunc; - } else { - $processManager->fork($deployerFunc); - } - $processNumber++; - } - } - } - $returnStatus = null; - while (count($processManager->getProcesses()) > 0) { - foreach ($processManager->getProcesses() as $process) { - if ($process->isCompleted()) { - $processManager->delete($process); - $returnStatus |= $process->getStatus(); - if ($queuedProcess = array_shift($processQueue)) { - $processManager->fork($queuedProcess); - } - if (count($processManager->getProcesses()) >= $this->getProcessesAmount()) { - break 1; - } - } - } - usleep(5000); - } - - return $returnStatus === Cli::RETURN_SUCCESS ?: Cli::RETURN_FAILURE; - } - - /** - * @return bool - */ - private function isCanBeParalleled() - { - return function_exists('pcntl_fork') && $this->getProcessesAmount() > 1; - } - - /** - * @return int - */ - private function getProcessesAmount() - { - $jobs = (int)$this->input->getOption(Options::JOBS_AMOUNT); - if ($jobs < 1) { - throw new \InvalidArgumentException( - Options::JOBS_AMOUNT . ' argument has invalid value. It must be greater than 0' - ); - } - return $jobs; - } } diff --git a/app/code/Magento/Deploy/Console/Command/DeployStaticOptionsInterface.php b/app/code/Magento/Deploy/Console/Command/DeployStaticOptionsInterface.php index 729ccbe8096286fa75adce196a1858371d64c732..25457f5505fb974240e4924c9c7713555ec8e97d 100644 --- a/app/code/Magento/Deploy/Console/Command/DeployStaticOptionsInterface.php +++ b/app/code/Magento/Deploy/Console/Command/DeployStaticOptionsInterface.php @@ -92,4 +92,9 @@ interface DeployStaticOptionsInterface * Force run of static deploy */ const FORCE_RUN = 'force'; + + /** + * Symlink locale if it not customized + */ + const SYMLINK_LOCALE = 'symlink-locale'; } diff --git a/app/code/Magento/Deploy/Model/Deploy/DeployInterface.php b/app/code/Magento/Deploy/Model/Deploy/DeployInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..16168c2b284c5bb7cd34d878d4448dae21d164d6 --- /dev/null +++ b/app/code/Magento/Deploy/Model/Deploy/DeployInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Deploy\Model\Deploy; + +interface DeployInterface +{ + /** + * Base locale option without customizations + */ + const DEPLOY_BASE_LOCALE = 'deploy_base_locale'; + + /** + * @param string $area + * @param string $themePath + * @param string $locale + * @return int + */ + public function deploy($area, $themePath, $locale); +} diff --git a/app/code/Magento/Deploy/Model/Deploy/LocaleDeploy.php b/app/code/Magento/Deploy/Model/Deploy/LocaleDeploy.php new file mode 100644 index 0000000000000000000000000000000000000000..aa112a700133185cf06980fb6b465de509630371 --- /dev/null +++ b/app/code/Magento/Deploy/Model/Deploy/LocaleDeploy.php @@ -0,0 +1,453 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Deploy\Model\Deploy; + +use Magento\Framework\App\Utility\Files; +use Magento\Framework\App\View\Asset\Publisher; +use Magento\Framework\View\Asset\ContentProcessorException; +use Magento\Framework\View\Asset\PreProcessor\AlternativeSourceInterface; +use Magento\Framework\View\Design\Theme\ThemeProviderInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Magento\Framework\Config\Theme; +use Magento\Deploy\Console\Command\DeployStaticOptionsInterface as Options; +use Magento\Framework\Translate\Js\Config as JsTranslationConfig; +use Magento\Framework\View\Asset\Minification; +use Psr\Log\LoggerInterface; +use Magento\Framework\Console\Cli; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) + */ +class LocaleDeploy implements DeployInterface +{ + /** + * @var int + */ + private $count = 0; + + /** + * @var int + */ + private $errorCount = 0; + + /** + * @var OutputInterface + */ + private $output; + + /** + * @var \Magento\Framework\View\Asset\Repository + */ + private $assetRepo; + + /** + * @var Publisher + */ + private $assetPublisher; + + /** + * @var \Magento\Framework\View\Asset\Bundle\Manager + */ + private $bundleManager; + + /** + * @var Files + */ + private $filesUtil; + + /** + * @var ThemeProviderInterface + */ + private $themeProvider; + + /** + * @var array + */ + private $options = []; + + /** + * @var JsTranslationConfig + */ + private $jsTranslationConfig; + + /** + * @var Minification + */ + private $minification; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var \Magento\Framework\View\Asset\RepositoryFactory + */ + private $assetRepoFactory; + + /** + * @var \Magento\RequireJs\Model\FileManagerFactory + */ + private $fileManagerFactory; + + /** + * @var \Magento\Framework\RequireJs\ConfigFactory + */ + private $requireJsConfigFactory; + + /** + * @var \Magento\Framework\View\DesignInterfaceFactory + */ + private $designFactory; + + /** + * @var \Magento\Framework\Locale\ResolverInterface + */ + private $localeResolver; + + /** + * @var \Magento\Framework\View\Asset\PreProcessor\AlternativeSourceInterface[] + */ + private $alternativeSources; + + /** + * @var array + */ + private static $fileExtensionOptionMap = [ + 'js' => Options::NO_JAVASCRIPT, + 'map' => Options::NO_JAVASCRIPT, + 'css' => Options::NO_CSS, + 'less' => Options::NO_LESS, + 'html' => Options::NO_HTML, + 'htm' => Options::NO_HTML, + 'jpg' => Options::NO_IMAGES, + 'jpeg' => Options::NO_IMAGES, + 'gif' => Options::NO_IMAGES, + 'png' => Options::NO_IMAGES, + 'ico' => Options::NO_IMAGES, + 'svg' => Options::NO_IMAGES, + 'eot' => Options::NO_FONTS, + 'ttf' => Options::NO_FONTS, + 'woff' => Options::NO_FONTS, + 'woff2' => Options::NO_FONTS, + 'md' => Options::NO_MISC, + 'jbf' => Options::NO_MISC, + 'csv' => Options::NO_MISC, + 'json' => Options::NO_MISC, + 'txt' => Options::NO_MISC, + 'htc' => Options::NO_MISC, + 'swf' => Options::NO_MISC, + 'LICENSE' => Options::NO_MISC, + '' => Options::NO_MISC, + ]; + + /** + * @param OutputInterface $output + * @param JsTranslationConfig $jsTranslationConfig + * @param Minification $minification + * @param \Magento\Framework\View\Asset\Repository $assetRepo + * @param \Magento\Framework\View\Asset\RepositoryFactory $assetRepoFactory + * @param \Magento\RequireJs\Model\FileManagerFactory $fileManagerFactory + * @param \Magento\Framework\RequireJs\ConfigFactory $requireJsConfigFactory + * @param Publisher $assetPublisher + * @param \Magento\Framework\View\Asset\Bundle\Manager $bundleManager + * @param ThemeProviderInterface $themeProvider + * @param LoggerInterface $logger + * @param Files $filesUtil + * @param \Magento\Framework\View\DesignInterfaceFactory $designFactory + * @param \Magento\Framework\Locale\ResolverInterface $localeResolver + * @param array $alternativeSources + * @param array $options + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + OutputInterface $output, + JsTranslationConfig $jsTranslationConfig, + Minification $minification, + \Magento\Framework\View\Asset\Repository $assetRepo, + \Magento\Framework\View\Asset\RepositoryFactory $assetRepoFactory, + \Magento\RequireJs\Model\FileManagerFactory $fileManagerFactory, + \Magento\Framework\RequireJs\ConfigFactory $requireJsConfigFactory, + \Magento\Framework\App\View\Asset\Publisher $assetPublisher, + \Magento\Framework\View\Asset\Bundle\Manager $bundleManager, + \Magento\Framework\View\Design\Theme\ThemeProviderInterface $themeProvider, + LoggerInterface $logger, + Files $filesUtil, + \Magento\Framework\View\DesignInterfaceFactory $designFactory, + \Magento\Framework\Locale\ResolverInterface $localeResolver, + array $alternativeSources, + $options = [] + ) { + $this->output = $output; + $this->assetRepo = $assetRepo; + $this->assetPublisher = $assetPublisher; + $this->bundleManager = $bundleManager; + $this->filesUtil = $filesUtil; + $this->jsTranslationConfig = $jsTranslationConfig; + $this->minification = $minification; + $this->logger = $logger; + $this->assetRepoFactory = $assetRepoFactory; + $this->fileManagerFactory = $fileManagerFactory; + $this->requireJsConfigFactory = $requireJsConfigFactory; + $this->themeProvider = $themeProvider; + $this->alternativeSources = array_map( + function (AlternativeSourceInterface $alternativeSource) { + return $alternativeSource; + }, + $alternativeSources + ); + $this->designFactory = $designFactory; + $this->localeResolver = $localeResolver; + $this->options = $options; + } + + /** + * {@inheritdoc} + */ + public function deploy($area, $themePath, $locale) + { + $this->output->writeln("=== {$area} -> {$themePath} -> {$locale} ==="); + + // emulate application locale needed for correct file path resolving + $this->localeResolver->setLocale($locale); + + $this->deployRequireJsConfig($area, $themePath); + $this->deployAppFiles($area, $themePath, $locale); + $this->deployLibFiles($area, $themePath, $locale); + + if (!$this->getOption(Options::NO_JAVASCRIPT)) { + if ($this->jsTranslationConfig->dictionaryEnabled()) { + $dictionaryFileName = $this->jsTranslationConfig->getDictionaryFileName(); + $this->deployFile($dictionaryFileName, $area, $themePath, $locale, null); + } + } + if (!$this->getOption(Options::NO_JAVASCRIPT)) { + $this->bundleManager->flush(); + } + $this->output->writeln("\nSuccessful: {$this->count} files; errors: {$this->errorCount}\n---\n"); + + return $this->errorCount ? Cli::RETURN_FAILURE : Cli::RETURN_SUCCESS; + } + + /** + * @param string $area + * @param string $themePath + * @return void + */ + private function deployRequireJsConfig($area, $themePath) + { + if (!$this->getOption(Options::DRY_RUN) && !$this->getOption(Options::NO_JAVASCRIPT)) { + $design = $this->designFactory->create()->setDesignTheme($themePath, $area); + $assetRepo = $this->assetRepoFactory->create(['design' => $design]); + /** @var \Magento\RequireJs\Model\FileManager $fileManager */ + $fileManager = $this->fileManagerFactory->create( + [ + 'config' => $this->requireJsConfigFactory->create( + [ + 'assetRepo' => $assetRepo, + 'design' => $design, + ] + ), + 'assetRepo' => $assetRepo, + ] + ); + $fileManager->createRequireJsConfigAsset(); + if ($this->minification->isEnabled('js')) { + $fileManager->createMinResolverAsset(); + } + } + } + + /** + * @param string $area + * @param string $themePath + * @param string $locale + * @return void + */ + private function deployAppFiles($area, $themePath, $locale) + { + foreach ($this->filesUtil->getStaticPreProcessingFiles() as $info) { + list($fileArea, $fileTheme, , $module, $filePath, $fullPath) = $info; + + if ($this->checkSkip($filePath)) { + continue; + } + + if ($this->isCanBeDeployed($fileArea, $fileTheme, $area, $themePath)) { + $compiledFile = $this->deployFile( + $filePath, + $area, + $themePath, + $locale, + $module, + $fullPath + ); + if ($compiledFile !== '' && !$this->checkSkip($compiledFile)) { + $this->deployFile($compiledFile, $area, $themePath, $locale, $module, $fullPath); + } + } + } + } + + /** + * @param string $fileArea + * @param string $fileTheme + * @param string $area + * @param string $themePath + * @return bool + */ + private function isCanBeDeployed($fileArea, $fileTheme, $area, $themePath) + { + return ($fileArea == $area || $fileArea == 'base') + && ($fileTheme == '' || $fileTheme == $themePath + || in_array( + $fileArea . Theme::THEME_PATH_SEPARATOR . $fileTheme, + $this->findAncestors($area . Theme::THEME_PATH_SEPARATOR . $themePath) + ) + ); + } + + /** + * @param string $area + * @param string $themePath + * @param string $locale + * @return void + */ + private function deployLibFiles($area, $themePath, $locale) + { + foreach ($this->filesUtil->getStaticLibraryFiles() as $filePath) { + + if ($this->checkSkip($filePath)) { + continue; + } + + $compiledFile = $this->deployFile($filePath, $area, $themePath, $locale, null); + + if ($compiledFile !== '' && !$this->checkSkip($compiledFile)) { + $this->deployFile($compiledFile, $area, $themePath, $locale, null); + } + } + } + + /** + * Deploy a static view file + * + * @param string $filePath + * @param string $area + * @param string $themePath + * @param string $locale + * @param string $module + * @param string|null $fullPath + * @return string + * + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function deployFile($filePath, $area, $themePath, $locale, $module, $fullPath = null) + { + $compiledFile = ''; + $extension = pathinfo($filePath, PATHINFO_EXTENSION); + + foreach ($this->alternativeSources as $name => $alternative) { + if (in_array($extension, $alternative->getAlternativesExtensionsNames(), true) + && strpos(basename($filePath), '_') !== 0 + ) { + $compiledFile = substr($filePath, 0, strlen($filePath) - strlen($extension) - 1); + $compiledFile = $compiledFile . '.' . $name; + } + } + + if ($this->output->isVeryVerbose()) { + $logMessage = "Processing file '$filePath' for area '$area', theme '$themePath', locale '$locale'"; + if ($module) { + $logMessage .= ", module '$module'"; + } + $this->output->writeln($logMessage); + } + + try { + $asset = $this->assetRepo->createAsset( + $filePath, + ['area' => $area, 'theme' => $themePath, 'locale' => $locale, 'module' => $module] + ); + if ($this->output->isVeryVerbose()) { + $this->output->writeln("\tDeploying the file to '{$asset->getPath()}'"); + } else { + $this->output->write('.'); + } + if ($this->getOption(Options::DRY_RUN)) { + $asset->getContent(); + } else { + $this->assetPublisher->publish($asset); + if (!$this->getOption(Options::NO_JAVASCRIPT)) { + $this->bundleManager->addAsset($asset); + } + } + $this->count++; + } catch (ContentProcessorException $exception) { + $pathInfo = $fullPath ?: $filePath; + $errorMessage = __('Compilation from source: ') . $pathInfo . PHP_EOL . $exception->getMessage(); + $this->errorCount++; + $this->output->write(PHP_EOL . PHP_EOL . $errorMessage . PHP_EOL, true); + + $this->logger->critical($errorMessage); + } catch (\Exception $exception) { + $this->output->write('.'); + if ($this->output->isVerbose()) { + $this->output->writeln($exception->getTraceAsString()); + } + $this->errorCount++; + } + + return $compiledFile; + } + + /** + * @param string $name + * @return mixed|null + */ + private function getOption($name) + { + return isset($this->options[$name]) ? $this->options[$name] : null; + } + + /** + * Check if skip flag is affecting file by extension + * + * @param string $filePath + * @return boolean + */ + private function checkSkip($filePath) + { + if ($filePath != '.') { + $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); + $option = isset(self::$fileExtensionOptionMap[$ext]) ? self::$fileExtensionOptionMap[$ext] : null; + + return $option ? $this->getOption($option) : false; + } + + return false; + } + + /** + * Find ancestor themes' full paths + * + * @param string $themeFullPath + * @return string[] + */ + private function findAncestors($themeFullPath) + { + $theme = $this->themeProvider->getThemeByFullPath($themeFullPath); + $ancestors = $theme->getInheritedThemes(); + $ancestorThemeFullPath = []; + foreach ($ancestors as $ancestor) { + $ancestorThemeFullPath[] = $ancestor->getFullPath(); + } + return $ancestorThemeFullPath; + } +} diff --git a/app/code/Magento/Deploy/Model/Deploy/LocaleQuickDeploy.php b/app/code/Magento/Deploy/Model/Deploy/LocaleQuickDeploy.php new file mode 100644 index 0000000000000000000000000000000000000000..8102afb363024b583068543ed319ade7e1e85965 --- /dev/null +++ b/app/code/Magento/Deploy/Model/Deploy/LocaleQuickDeploy.php @@ -0,0 +1,152 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Deploy\Model\Deploy; + +use Magento\Deploy\Model\DeployManager; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\Utility\Files; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Magento\Framework\Console\Cli; +use Magento\Deploy\Console\Command\DeployStaticOptionsInterface as Options; +use \Magento\Framework\RequireJs\Config as RequireJsConfig; + +class LocaleQuickDeploy implements DeployInterface +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var WriteInterface + */ + private $staticDirectory; + + /** + * @var OutputInterface + */ + private $output; + + /** + * @var array + */ + private $options = []; + + /** + * @param Filesystem $filesystem + * @param OutputInterface $output + * @param array $options + */ + public function __construct(\Magento\Framework\Filesystem $filesystem, OutputInterface $output, $options = []) + { + $this->filesystem = $filesystem; + $this->output = $output; + $this->options = $options; + } + + /** + * @return WriteInterface + */ + private function getStaticDirectory() + { + if ($this->staticDirectory === null) { + $this->staticDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::STATIC_VIEW); + } + + return $this->staticDirectory; + } + + /** + * {@inheritdoc} + */ + public function deploy($area, $themePath, $locale) + { + if (isset($this->options[Options::DRY_RUN]) && $this->options[Options::DRY_RUN]) { + return Cli::RETURN_SUCCESS; + } + + $this->output->writeln("=== {$area} -> {$themePath} -> {$locale} ==="); + + if (!isset($this->options[self::DEPLOY_BASE_LOCALE])) { + throw new \InvalidArgumentException('Deploy base locale must be set for Quick Deploy'); + } + $processedFiles = 0; + $errorAmount = 0; + + $baseLocale = $this->options[self::DEPLOY_BASE_LOCALE]; + $newLocalePath = $this->getLocalePath($area, $themePath, $locale); + $baseLocalePath = $this->getLocalePath($area, $themePath, $baseLocale); + $baseRequireJsPath = RequireJsConfig::DIR_NAME . DIRECTORY_SEPARATOR . $baseLocalePath; + $newRequireJsPath = RequireJsConfig::DIR_NAME . DIRECTORY_SEPARATOR . $newLocalePath; + + $this->deleteLocaleResource($newLocalePath); + $this->deleteLocaleResource($newRequireJsPath); + + if (isset($this->options[Options::SYMLINK_LOCALE]) && $this->options[Options::SYMLINK_LOCALE]) { + $this->getStaticDirectory()->createSymlink($baseLocalePath, $newLocalePath); + $this->getStaticDirectory()->createSymlink($baseRequireJsPath, $newRequireJsPath); + + $this->output->writeln("\nSuccessful symlinked\n---\n"); + } else { + $localeFiles = array_merge( + $this->getStaticDirectory()->readRecursively($baseLocalePath), + $this->getStaticDirectory()->readRecursively($baseRequireJsPath) + ); + foreach ($localeFiles as $path) { + if ($this->getStaticDirectory()->isFile($path)) { + $destination = $this->replaceLocaleInPath($path, $baseLocale, $locale); + $this->getStaticDirectory()->copyFile($path, $destination); + $processedFiles++; + } + } + + $this->output->writeln("\nSuccessful copied: {$processedFiles} files; errors: {$errorAmount}\n---\n"); + } + + return Cli::RETURN_SUCCESS; + } + + /** + * @param string $path + * @return void + */ + private function deleteLocaleResource($path) + { + if ($this->getStaticDirectory()->isExist($path)) { + $absolutePath = $this->getStaticDirectory()->getAbsolutePath($path); + if (is_link($absolutePath)) { + $this->getStaticDirectory()->getDriver()->deleteFile($absolutePath); + } else { + $this->getStaticDirectory()->getDriver()->deleteDirectory($absolutePath); + } + } + } + + /** + * @param string $path + * @param string $search + * @param string $replace + * @return string + */ + private function replaceLocaleInPath($path, $search, $replace) + { + return preg_replace('~' . $search . '~', $replace, $path, 1); + } + + /** + * @param string $area + * @param string $themePath + * @param string $locale + * @return string + */ + private function getLocalePath($area, $themePath, $locale) + { + return $area . DIRECTORY_SEPARATOR . $themePath . DIRECTORY_SEPARATOR . $locale; + } +} diff --git a/app/code/Magento/Deploy/Model/Deploy/TemplateMinifier.php b/app/code/Magento/Deploy/Model/Deploy/TemplateMinifier.php new file mode 100644 index 0000000000000000000000000000000000000000..7a5d8f92f3c32d3b9f45f5addb5c4f76894cb931 --- /dev/null +++ b/app/code/Magento/Deploy/Model/Deploy/TemplateMinifier.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Deploy\Model\Deploy; + +use Magento\Framework\View\Template\Html\MinifierInterface; +use Magento\Framework\App\Utility\Files; + +class TemplateMinifier +{ + /** + * @var Files + */ + private $filesUtils; + + /** + * @var MinifierInterface + */ + private $htmlMinifier; + + /** + * @param Files $filesUtils + * @param MinifierInterface $htmlMinifier + */ + public function __construct( + Files $filesUtils, + MinifierInterface $htmlMinifier + ) { + $this->filesUtils = $filesUtils; + $this->htmlMinifier = $htmlMinifier; + } + + /** + * Minify template files + * @return int + */ + public function minifyTemplates() + { + $minified = 0; + foreach ($this->filesUtils->getPhtmlFiles(false, false) as $template) { + $this->htmlMinifier->minify($template); + $minified++; + } + return $minified; + } +} diff --git a/app/code/Magento/Deploy/Model/DeployManager.php b/app/code/Magento/Deploy/Model/DeployManager.php new file mode 100644 index 0000000000000000000000000000000000000000..c54aa38f5a33ab21283537c90c717f3844d08a0e --- /dev/null +++ b/app/code/Magento/Deploy/Model/DeployManager.php @@ -0,0 +1,213 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Deploy\Model; + +use Magento\Framework\App\View\Deployment\Version\StorageInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Magento\Deploy\Console\Command\DeployStaticOptionsInterface as Options; +use Magento\Deploy\Model\Deploy\TemplateMinifier; +use Magento\Framework\App\State; + +class DeployManager +{ + /** + * @var array + */ + private $packages = []; + + /** + * @var OutputInterface + */ + private $output; + + /** + * @var array + */ + private $options; + + /** + * @var StorageInterface + */ + private $versionStorage; + + /** + * @var DeployStrategyProviderFactory + */ + private $deployStrategyProviderFactory; + + /** + * @var ProcessQueueManagerFactory + */ + private $processQueueManagerFactory; + + /** + * @var TemplateMinifier + */ + private $templateMinifier; + + /** + * @var bool + */ + private $idDryRun; + + /** + * @var State + */ + private $state; + + /** + * @param OutputInterface $output + * @param StorageInterface $versionStorage + * @param DeployStrategyProviderFactory $deployStrategyProviderFactory + * @param ProcessQueueManagerFactory $processQueueManagerFactory + * @param TemplateMinifier $templateMinifier + * @param State $state + * @param array $options + */ + public function __construct( + OutputInterface $output, + StorageInterface $versionStorage, + DeployStrategyProviderFactory $deployStrategyProviderFactory, + ProcessQueueManagerFactory $processQueueManagerFactory, + TemplateMinifier $templateMinifier, + State $state, + array $options + ) { + $this->output = $output; + $this->options = $options; + $this->versionStorage = $versionStorage; + $this->deployStrategyProviderFactory = $deployStrategyProviderFactory; + $this->processQueueManagerFactory = $processQueueManagerFactory; + $this->templateMinifier = $templateMinifier; + $this->state = $state; + $this->idDryRun = !empty($this->options[Options::DRY_RUN]); + } + + /** + * Add package tie to area and theme + * + * @param string $area + * @param string $themePath + * @param string $locale + * @return void + */ + public function addPack($area, $themePath, $locale) + { + $this->packages[$area . '-' . $themePath][$locale] = [$area, $themePath]; + } + + /** + * Deploy local packages with chosen deploy strategy + * @return int + */ + public function deploy() + { + if ($this->idDryRun) { + $this->output->writeln('Dry run. Nothing will be recorded to the target directory.'); + } + + /** @var DeployStrategyProvider $strategyProvider */ + $strategyProvider = $this->deployStrategyProviderFactory->create( + ['output' => $this->output, 'options' => $this->options] + ); + + if ($this->isCanBeParalleled()) { + $result = $this->runInParallel($strategyProvider); + } else { + $result = 0; + foreach ($this->packages as $package) { + $locales = array_keys($package); + list($area, $themePath) = current($package); + foreach ($strategyProvider->getDeployStrategies($area, $themePath, $locales) as $locale => $strategy) { + $result |= $this->state->emulateAreaCode( + $area, + [$strategy, 'deploy'], + [$area, $themePath, $locale] + ); + } + } + } + + $this->minifyTemplates(); + $this->saveDeployedVersion(); + + return $result; + } + + /** + * @return void + */ + private function minifyTemplates() + { + $noHtmlMinify = isset($this->options[Options::NO_HTML_MINIFY]) ? $this->options[Options::NO_HTML_MINIFY] : null; + if (!$noHtmlMinify && !$this->idDryRun) { + $this->output->writeln('=== Minify templates ==='); + $minified = $this->templateMinifier->minifyTemplates(); + $this->output->writeln("\nSuccessful: {$minified} files modified\n---\n"); + } + } + + /** + * @param DeployStrategyProvider $strategyProvider + * @return int + */ + private function runInParallel(DeployStrategyProvider $strategyProvider) + { + $processQueueManager = $this->processQueueManagerFactory->create( + ['maxProcesses' => $this->getProcessesAmount()] + ); + foreach ($this->packages as $package) { + $locales = array_keys($package); + list($area, $themePath) = current($package); + $baseStrategy = null; + $dependentStrategy = []; + foreach ($strategyProvider->getDeployStrategies($area, $themePath, $locales) as $locale => $strategy) { + $deploymentFunc = function () use ($area, $themePath, $locale, $strategy) { + return $this->state->emulateAreaCode($area, [$strategy, 'deploy'], [$area, $themePath, $locale]); + }; + if (null === $baseStrategy) { + $baseStrategy = $deploymentFunc; + } else { + $dependentStrategy[] = $deploymentFunc; + } + + } + $processQueueManager->addTaskToQueue($baseStrategy, $dependentStrategy); + } + + return $processQueueManager->process(); + } + + /** + * @return bool + */ + private function isCanBeParalleled() + { + return function_exists('pcntl_fork') && $this->getProcessesAmount() > 1; + } + + /** + * @return int + */ + private function getProcessesAmount() + { + return isset($this->options[Options::JOBS_AMOUNT]) ? (int)$this->options[Options::JOBS_AMOUNT] : 0; + } + + /** + * Save version of deployed files + * @return void + */ + private function saveDeployedVersion() + { + if (!$this->idDryRun) { + $version = (new \DateTime())->getTimestamp(); + $this->output->writeln("New version of deployed files: {$version}"); + $this->versionStorage->save($version); + } + } +} diff --git a/app/code/Magento/Deploy/Model/DeployStrategyFactory.php b/app/code/Magento/Deploy/Model/DeployStrategyFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..884ff5b6284d4dae3a3983e0a08ccfa8b68cf306 --- /dev/null +++ b/app/code/Magento/Deploy/Model/DeployStrategyFactory.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Deploy\Model; + +use Magento\Deploy\Model\Deploy\DeployInterface; +use Magento\Framework\Exception\InputException; +use Magento\Framework\ObjectManagerInterface; + +class DeployStrategyFactory +{ + /** + * Standard deploy strategy + */ + const DEPLOY_STRATEGY_STANDARD = 'standard'; + + /** + * Quick deploy strategy + */ + const DEPLOY_STRATEGY_QUICK = 'quick'; + + /** + * @param ObjectManagerInterface $objectManager + */ + public function __construct(ObjectManagerInterface $objectManager) + { + $this->objectManager = $objectManager; + } + + /** + * @param string $type + * @param array $arguments + * @return DeployInterface + * @throws InputException + */ + public function create($type, array $arguments = []) + { + $strategyMap = [ + self::DEPLOY_STRATEGY_STANDARD => Deploy\LocaleDeploy::class, + self::DEPLOY_STRATEGY_QUICK => Deploy\LocaleQuickDeploy::class, + ]; + + if (!isset($strategyMap[$type])) { + throw new InputException(__('Wrong deploy strategy type: %1', $type)); + } + + return $this->objectManager->create($strategyMap[$type], $arguments); + } +} diff --git a/app/code/Magento/Deploy/Model/DeployStrategyProvider.php b/app/code/Magento/Deploy/Model/DeployStrategyProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..9893aa1768f55d16f5ca0119788c54287cc6a16f --- /dev/null +++ b/app/code/Magento/Deploy/Model/DeployStrategyProvider.php @@ -0,0 +1,193 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Deploy\Model; + +use Magento\Deploy\Model\Deploy\DeployInterface; +use Magento\Framework\Component\ComponentRegistrar; +use Magento\Framework\Module\Dir; +use Magento\Framework\View\Design\Fallback\Rule\RuleInterface; +use Magento\Framework\View\DesignInterface; +use Magento\Framework\View\Design\Fallback\RulePool; +use Symfony\Component\Console\Output\OutputInterface; + +class DeployStrategyProvider +{ + /** + * @var RulePool + */ + private $rulePool; + + /** + * @var RuleInterface + */ + private $fallBackRule; + + /** + * @var array + */ + private $moduleDirectories; + + /** + * @var DesignInterface + */ + private $design; + + /** + * @var OutputInterface + */ + private $output; + + /** + * @var array + */ + private $options; + + /** + * @var DeployStrategyFactory + */ + private $deployStrategyFactory; + + /** + * @param OutputInterface $output + * @param RulePool $rulePool + * @param DesignInterface $design + * @param DeployStrategyFactory $deployStrategyFactory + * @param array $options + */ + public function __construct( + OutputInterface $output, + RulePool $rulePool, + DesignInterface $design, + DeployStrategyFactory $deployStrategyFactory, + array $options + ) { + $this->rulePool = $rulePool; + $this->design = $design; + $this->output = $output; + $this->options = $options; + $this->deployStrategyFactory = $deployStrategyFactory; + } + + /** + * @param string $area + * @param string $themePath + * @param array $locales + * @return DeployInterface[] + */ + public function getDeployStrategies($area, $themePath, array $locales) + { + if (count($locales) == 1) { + $locale = current($locales); + return [$locale => $this->getDeployStrategy(DeployStrategyFactory::DEPLOY_STRATEGY_STANDARD)]; + } + + $baseLocale = null; + $deployStrategies = []; + + foreach ($locales as $locale) { + $hasCustomization = false; + foreach ($this->getCustomizationDirectories($area, $themePath, $locale) as $directory) { + if (glob($directory . DIRECTORY_SEPARATOR . '*', GLOB_NOSORT)) { + $hasCustomization = true; + break; + } + } + if ($baseLocale === null && !$hasCustomization) { + $baseLocale = $locale; + } else { + $deployStrategies[$locale] = $hasCustomization + ? DeployStrategyFactory::DEPLOY_STRATEGY_STANDARD + : DeployStrategyFactory::DEPLOY_STRATEGY_QUICK; + } + } + $deployStrategies = array_merge( + [$baseLocale => DeployStrategyFactory::DEPLOY_STRATEGY_STANDARD], + $deployStrategies + ); + + return array_map(function ($strategyType) use ($area, $baseLocale) { + return $this->getDeployStrategy($strategyType, $baseLocale); + }, $deployStrategies); + } + + /** + * @param array $params + * @return array + */ + private function getLocaleDirectories($params) + { + $dirs = $this->getFallbackRule()->getPatternDirs($params); + + return array_filter($dirs, function ($dir) { + return strpos($dir, Dir::MODULE_I18N_DIR); + }); + } + + /** + * Get directories which can contains theme customization + * @param string $area + * @param string $themePath + * @param string $locale + * @return array + */ + private function getCustomizationDirectories($area, $themePath, $locale) + { + $customizationDirectories = []; + $this->design->setDesignTheme($themePath, $area); + + $params = ['area' => $area, 'theme' => $this->design->getDesignTheme(), 'locale' => $locale]; + foreach ($this->getLocaleDirectories($params) as $patternDir) { + $customizationDirectories[] = $patternDir; + } + + if ($this->moduleDirectories === null) { + $this->moduleDirectories = []; + $componentRegistrar = new ComponentRegistrar(); + $this->moduleDirectories = array_keys($componentRegistrar->getPaths(ComponentRegistrar::MODULE)); + } + + foreach ($this->moduleDirectories as $moduleDir) { + $params['module_name'] = $moduleDir; + $patternDirs = $this->getLocaleDirectories($params); + foreach ($patternDirs as $patternDir) { + $customizationDirectories[] = $patternDir; + } + } + + return $customizationDirectories; + } + + /** + * @return \Magento\Framework\View\Design\Fallback\Rule\RuleInterface + */ + private function getFallbackRule() + { + if (null === $this->fallBackRule) { + $this->fallBackRule = $this->rulePool->getRule(RulePool::TYPE_STATIC_FILE); + } + + return $this->fallBackRule; + } + + /** + * @param string $type + * @param null|string $baseLocale + * @return DeployInterface + */ + private function getDeployStrategy($type, $baseLocale = null) + { + $options = $this->options; + if ($baseLocale) { + $options[DeployInterface::DEPLOY_BASE_LOCALE] = $baseLocale; + } + + return $this->deployStrategyFactory->create( + $type, + ['output' => $this->output, 'options' => $options] + ); + } +} diff --git a/app/code/Magento/Deploy/Model/Deployer.php b/app/code/Magento/Deploy/Model/Deployer.php index bef65532fd4238edf75c84ff01f6846b878faf04..9c1781dcb08ad14b40753478e3997c68b3a0b206 100644 --- a/app/code/Magento/Deploy/Model/Deployer.php +++ b/app/code/Magento/Deploy/Model/Deployer.php @@ -6,125 +6,41 @@ namespace Magento\Deploy\Model; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\View\Asset\ContentProcessorException; +use Magento\Deploy\Console\Command\DeployStaticOptionsInterface; use Magento\Framework\View\Asset\PreProcessor\AlternativeSourceInterface; use Magento\Framework\App\ObjectManagerFactory; use Magento\Framework\App\View\Deployment\Version; -use Magento\Framework\App\View\Asset\Publisher; use Magento\Framework\App\Utility\Files; -use Magento\Framework\Config\Theme; -use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Translate\Js\Config as JsTranslationConfig; use Symfony\Component\Console\Output\OutputInterface; -use Psr\Log\LoggerInterface; -use Magento\Framework\View\Asset\Minification; use Magento\Framework\App\ObjectManager; -use Magento\Framework\View\Asset\ConfigInterface; -use Magento\Deploy\Console\Command\DeployStaticOptionsInterface as Options; +use Magento\Deploy\Model\DeployManagerFactory; /** * A service for deploying Magento static view files for production mode * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @SuppressWarnings(PHPMD.UnusedLocalVariable) - * @SuppressWarnings(PHPMD.TooManyFields) + * @deprecated + * @see Use DeployManager::deploy instead */ class Deployer { - /** @var Files */ - private $filesUtil; - - /** @var ObjectManagerFactory */ - private $omFactory; - /** @var OutputInterface */ private $output; - /** @var Version\StorageInterface */ - private $versionStorage; - - /** @var \Magento\Framework\View\Asset\Repository */ - private $assetRepo; - - /** @var Publisher */ - private $assetPublisher; - - /** @var \Magento\Framework\View\Asset\Bundle\Manager */ - private $bundleManager; - - /** @var int */ - private $count; - - /** @var int */ - private $errorCount; - - /** @var \Magento\Framework\View\Template\Html\MinifierInterface */ - private $htmlMinifier; - - /** - * @var ObjectManagerInterface - */ - private $objectManager; - /** * @var JsTranslationConfig */ protected $jsTranslationConfig; - /** - * @var AlternativeSourceInterface[] - */ - private $alternativeSources; - /** * @var array */ - private static $fileExtensionOptionMap = [ - 'js' => Options::NO_JAVASCRIPT, - 'map' => Options::NO_JAVASCRIPT, - 'css' => Options::NO_CSS, - 'less' => Options::NO_LESS, - 'html' => Options::NO_HTML, - 'htm' => Options::NO_HTML, - 'jpg' => Options::NO_IMAGES, - 'jpeg' => Options::NO_IMAGES, - 'gif' => Options::NO_IMAGES, - 'png' => Options::NO_IMAGES, - 'ico' => Options::NO_IMAGES, - 'svg' => Options::NO_IMAGES, - 'eot' => Options::NO_FONTS, - 'ttf' => Options::NO_FONTS, - 'woff' => Options::NO_FONTS, - 'woff2' => Options::NO_FONTS, - 'md' => Options::NO_MISC, - 'jbf' => Options::NO_MISC, - 'csv' => Options::NO_MISC, - 'json' => Options::NO_MISC, - 'txt' => Options::NO_MISC, - 'htc' => Options::NO_MISC, - 'swf' => Options::NO_MISC, - 'LICENSE' => Options::NO_MISC, - '' => Options::NO_MISC, - ]; - - /** - * @var Minification - */ - private $minification; - - /** - * @var LoggerInterface - */ - private $logger; - - /** @var ConfigInterface */ - private $assetConfig; + private $options; /** - * @var array + * @var DeployManagerFactory */ - private $options; + private $deployManagerFactory; /** * Constructor @@ -134,7 +50,9 @@ class Deployer * @param Version\StorageInterface $versionStorage * @param JsTranslationConfig $jsTranslationConfig * @param AlternativeSourceInterface[] $alternativeSources + * @param DeployManagerFactory $deployManagerFactory * @param array $options + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( Files $filesUtil, @@ -142,56 +60,29 @@ class Deployer Version\StorageInterface $versionStorage, JsTranslationConfig $jsTranslationConfig, array $alternativeSources, + DeployManagerFactory $deployManagerFactory = null, $options = [] ) { - $this->filesUtil = $filesUtil; $this->output = $output; - $this->versionStorage = $versionStorage; - $this->jsTranslationConfig = $jsTranslationConfig; + $this->deployManagerFactory = $deployManagerFactory; if (is_array($options)) { $this->options = $options; } else { // backward compatibility support - $this->options = [Options::DRY_RUN => (bool)$options]; + $this->options = [DeployStaticOptionsInterface::DRY_RUN => (bool)$options]; } - $this->parentTheme = []; - - array_map( - function (AlternativeSourceInterface $alternative) { - }, - $alternativeSources - ); - $this->alternativeSources = $alternativeSources; - } /** - * @param string $name - * @return mixed|null + * @return \Magento\Deploy\Model\DeployManagerFactory */ - private function getOption($name) + private function getDeployManagerFactory() { - return isset($this->options[$name]) ? $this->options[$name] : null; - } - - /** - * Check if skip flag is affecting file by extension - * - * @param string $filePath - * @return boolean - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - */ - private function checkSkip($filePath) - { - if ($filePath != '.') { - $ext = pathinfo($filePath, PATHINFO_EXTENSION); - $option = isset(self::$fileExtensionOptionMap[$ext]) ? self::$fileExtensionOptionMap[$ext] : null; - - return $option ? $this->getOption($option) : false; + if (null === $this->deployManagerFactory) { + $this->deployManagerFactory = ObjectManager::getInstance()->get(DeployManagerFactory::class); } - return false; + return $this->deployManagerFactory; } /** @@ -201,170 +92,24 @@ class Deployer * @param array $locales * @param array $deployableAreaThemeMap * @return int - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @deprecated */ public function deploy(ObjectManagerFactory $omFactory, array $locales, array $deployableAreaThemeMap = []) { - $this->omFactory = $omFactory; - - if ($this->getOption(Options::DRY_RUN)) { - $this->output->writeln('Dry run. Nothing will be recorded to the target directory.'); - } - $libFiles = $this->filesUtil->getStaticLibraryFiles(); - $appFiles = $this->filesUtil->getStaticPreProcessingFiles(); + /** @var DeployManager $deployerManager */ + $deployerManager = $this->getDeployManagerFactory()->create( + ['options' => $this->options, 'output' => $this->output] + ); foreach ($deployableAreaThemeMap as $area => $themes) { - $this->emulateApplicationArea($area); foreach ($locales as $locale) { - $this->emulateApplicationLocale($locale, $area); foreach ($themes as $themePath) { - - $this->output->writeln("=== {$area} -> {$themePath} -> {$locale} ==="); - $this->count = 0; - $this->errorCount = 0; - - /** @var \Magento\Theme\Model\View\Design $design */ - $design = $this->objectManager->create(\Magento\Theme\Model\View\Design::class); - $design->setDesignTheme($themePath, $area); - - $assetRepo = $this->objectManager->create( - \Magento\Framework\View\Asset\Repository::class, - [ - 'design' => $design, - ] - ); - /** @var \Magento\RequireJs\Model\FileManager $fileManager */ - $fileManager = $this->objectManager->create( - \Magento\RequireJs\Model\FileManager::class, - [ - 'config' => $this->objectManager->create( - \Magento\Framework\RequireJs\Config::class, - [ - 'assetRepo' => $assetRepo, - 'design' => $design, - ] - ), - 'assetRepo' => $assetRepo, - ] - ); - $fileManager->createRequireJsConfigAsset(); - - foreach ($appFiles as $info) { - list($fileArea, $fileTheme, , $module, $filePath, $fullPath) = $info; - - if ($this->checkSkip($filePath)) { - continue; - } - - if (($fileArea == $area || $fileArea == 'base') && - ($fileTheme == '' || $fileTheme == $themePath || - in_array( - $fileArea . Theme::THEME_PATH_SEPARATOR . $fileTheme, - $this->findAncestors($area . Theme::THEME_PATH_SEPARATOR . $themePath) - )) - ) { - $compiledFile = $this->deployFile( - $filePath, - $area, - $themePath, - $locale, - $module, - $fullPath - ); - if ($compiledFile !== '') { - $this->deployFile($compiledFile, $area, $themePath, $locale, $module, $fullPath); - } - } - } - foreach ($libFiles as $filePath) { - - if ($this->checkSkip($filePath)) { - continue; - } - - $compiledFile = $this->deployFile($filePath, $area, $themePath, $locale, null); - - if ($compiledFile !== '') { - $this->deployFile($compiledFile, $area, $themePath, $locale, null); - } - } - if (!$this->getOption(Options::NO_JAVASCRIPT)) { - if ($this->jsTranslationConfig->dictionaryEnabled()) { - $dictionaryFileName = $this->jsTranslationConfig->getDictionaryFileName(); - $this->deployFile($dictionaryFileName, $area, $themePath, $locale, null); - } - if ($this->getMinification()->isEnabled('js')) { - $fileManager->createMinResolverAsset(); - } - } - $this->bundleManager->flush(); - $this->output->writeln("\nSuccessful: {$this->count} files; errors: {$this->errorCount}\n---\n"); + $deployerManager->addPack($area, $themePath, $locale); } } } - if (!($this->getOption(Options::NO_HTML_MINIFY) ?: !$this->getAssetConfig()->isMinifyHtml())) { - $this->output->writeln('=== Minify templates ==='); - $this->count = 0; - foreach ($this->filesUtil->getPhtmlFiles(false, false) as $template) { - $this->htmlMinifier->minify($template); - if ($this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { - $this->output->writeln($template . " minified\n"); - } else { - $this->output->write('.'); - } - $this->count++; - } - $this->output->writeln("\nSuccessful: {$this->count} files modified\n---\n"); - } - - $version = (new \DateTime())->getTimestamp(); - $this->output->writeln("New version of deployed files: {$version}"); - if (!$this->getOption(Options::DRY_RUN)) { - $this->versionStorage->save($version); - } - - if ($this->errorCount > 0) { - // we must have an exit code higher than zero to indicate something was wrong - return \Magento\Framework\Console\Cli::RETURN_FAILURE; - } - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; - } - - /** - * Get Minification instance - * - * @deprecated - * @return Minification - */ - private function getMinification() - { - if (null === $this->minification) { - $this->minification = ObjectManager::getInstance()->get(Minification::class); - } - - return $this->minification; - } - - /** - * Emulate application area and various services that are necessary for populating files - * - * @param string $areaCode - * @return void - */ - private function emulateApplicationArea($areaCode) - { - $this->objectManager = $this->omFactory->create( - [\Magento\Framework\App\State::PARAM_MODE => \Magento\Framework\App\State::MODE_PRODUCTION] - ); - /** @var \Magento\Framework\App\State $appState */ - $appState = $this->objectManager->get(\Magento\Framework\App\State::class); - $appState->setAreaCode($areaCode); - $this->assetRepo = $this->objectManager->get(\Magento\Framework\View\Asset\Repository::class); - $this->assetPublisher = $this->objectManager->create(\Magento\Framework\App\View\Asset\Publisher::class); - $this->htmlMinifier = $this->objectManager->get(\Magento\Framework\View\Template\Html\MinifierInterface::class); - $this->bundleManager = $this->objectManager->get(\Magento\Framework\View\Asset\Bundle\Manager::class); + return $deployerManager->deploy(); } /** @@ -373,146 +118,10 @@ class Deployer * @param string $locale * @param string $area * @return void - */ - protected function emulateApplicationLocale($locale, $area) - { - /** @var \Magento\Framework\TranslateInterface $translator */ - $translator = $this->objectManager->get(\Magento\Framework\TranslateInterface::class); - $translator->setLocale($locale); - $translator->loadData($area, true); - /** @var \Magento\Framework\Locale\ResolverInterface $localeResolver */ - $localeResolver = $this->objectManager->get(\Magento\Framework\Locale\ResolverInterface::class); - $localeResolver->setLocale($locale); - } - - /** - * Deploy a static view file - * - * @param string $filePath - * @param string $area - * @param string $themePath - * @param string $locale - * @param string $module - * @param string|null $fullPath - * @return string - * @throws \InvalidArgumentException - * @throws LocalizedException - * - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - private function deployFile($filePath, $area, $themePath, $locale, $module, $fullPath = null) - { - $compiledFile = ''; - $extension = pathinfo($filePath, PATHINFO_EXTENSION); - - foreach ($this->alternativeSources as $name => $alternative) { - if (in_array($extension, $alternative->getAlternativesExtensionsNames(), true) - && strpos(basename($filePath), '_') !== 0 - ) { - $compiledFile = substr($filePath, 0, strlen($filePath) - strlen($extension) - 1); - $compiledFile = $compiledFile . '.' . $name; - } - } - - if ($this->output->isVeryVerbose()) { - $logMessage = "Processing file '$filePath' for area '$area', theme '$themePath', locale '$locale'"; - if ($module) { - $logMessage .= ", module '$module'"; - } - $this->output->writeln($logMessage); - } - - try { - $asset = $this->assetRepo->createAsset( - $filePath, - ['area' => $area, 'theme' => $themePath, 'locale' => $locale, 'module' => $module] - ); - if ($this->output->isVeryVerbose()) { - $this->output->writeln("\tDeploying the file to '{$asset->getPath()}'"); - } else { - $this->output->write('.'); - } - if ($this->getOption(Options::DRY_RUN)) { - $asset->getContent(); - } else { - $this->assetPublisher->publish($asset); - $this->bundleManager->addAsset($asset); - } - $this->count++; - } catch (ContentProcessorException $exception) { - $pathInfo = $fullPath ?: $filePath; - $errorMessage = __('Compilation from source: ') . $pathInfo - . PHP_EOL . $exception->getMessage(); - $this->errorCount++; - $this->output->write(PHP_EOL . PHP_EOL . $errorMessage . PHP_EOL, true); - - $this->getLogger()->critical($errorMessage); - } catch (\Exception $exception) { - $this->output->write('.'); - $this->verboseLog($exception->getTraceAsString()); - $this->errorCount++; - } - - return $compiledFile; - } - - /** - * Find ancestor themes' full paths - * - * @param string $themeFullPath - * @return string[] - */ - private function findAncestors($themeFullPath) - { - /** @var \Magento\Framework\View\Design\Theme\ListInterface $themeCollection */ - $themeCollection = $this->objectManager->get(\Magento\Framework\View\Design\Theme\ListInterface::class); - $theme = $themeCollection->getThemeByFullPath($themeFullPath); - $ancestors = $theme->getInheritedThemes(); - $ancestorThemeFullPath = []; - foreach ($ancestors as $ancestor) { - $ancestorThemeFullPath[] = $ancestor->getFullPath(); - } - return $ancestorThemeFullPath; - } - - /** - * @return \Magento\Framework\View\Asset\ConfigInterface - * @deprecated - */ - private function getAssetConfig() - { - if (null === $this->assetConfig) { - $this->assetConfig = ObjectManager::getInstance()->get(ConfigInterface::class); - } - return $this->assetConfig; - } - - /** - * Verbose log - * - * @param string $message - * @return void - */ - private function verboseLog($message) - { - if ($this->output->isVerbose()) { - $this->output->writeln($message); - } - } - - /** - * Retrieves LoggerInterface instance - * - * @return LoggerInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @deprecated */ - private function getLogger() + protected function emulateApplicationLocale($locale, $area) { - if (!$this->logger) { - $this->logger = $this->objectManager->get(LoggerInterface::class); - } - - return $this->logger; } } diff --git a/app/code/Magento/Deploy/Model/ProcessManager.php b/app/code/Magento/Deploy/Model/ProcessManager.php index cff7e347ade9d9133e6d6cacb837118dd7178c23..9490f6dc67101eb63a14e8b6123e11b11008feeb 100644 --- a/app/code/Magento/Deploy/Model/ProcessManager.php +++ b/app/code/Magento/Deploy/Model/ProcessManager.php @@ -11,6 +11,20 @@ class ProcessManager /** @var Process[] */ private $processes = []; + /** + * @var ProcessFactory + */ + private $processFactory; + + /** + * ProcessManager constructor. + * @param ProcessFactory $processFactory + */ + public function __construct(ProcessFactory $processFactory) + { + $this->processFactory = $processFactory; + } + /** * Forks the currently running process. * @@ -66,7 +80,7 @@ class ProcessManager */ private function createProcess(callable $handler) { - return new Process($handler); + return $this->processFactory->create(['handler' => $handler]); } /** diff --git a/app/code/Magento/Deploy/Model/ProcessQueueManager.php b/app/code/Magento/Deploy/Model/ProcessQueueManager.php new file mode 100644 index 0000000000000000000000000000000000000000..c96a2dbb30bd0c169dee84ee9bd358e6dac49228 --- /dev/null +++ b/app/code/Magento/Deploy/Model/ProcessQueueManager.php @@ -0,0 +1,160 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Deploy\Model; + +use Magento\Framework\App\ResourceConnection; + +class ProcessQueueManager +{ + /** + * Default max amount of processes + */ + const DEFAULT_MAX_PROCESSES_AMOUNT = 4; + + /** + * @var ProcessTask[] + */ + private $tasksQueue = []; + + /** + * @var ProcessTask[] + */ + private $processTaskMap = []; + + /** + * @var int + */ + private $maxProcesses; + + /** + * @var ProcessManager + */ + private $processManager; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var ProcessTaskFactory + */ + private $processTaskFactory; + + /** + * @param ProcessManager $processManager + * @param ResourceConnection $resourceConnection + * @param ProcessTaskFactory $processTaskFactory + * @param int $maxProcesses + */ + public function __construct( + ProcessManager $processManager, + ResourceConnection $resourceConnection, + ProcessTaskFactory $processTaskFactory, + $maxProcesses = self::DEFAULT_MAX_PROCESSES_AMOUNT + ) { + $this->processManager = $processManager; + $this->resourceConnection = $resourceConnection; + $this->processTaskFactory = $processTaskFactory; + $this->maxProcesses = $maxProcesses; + } + + /** + * @param callable $task + * @param callable[] $dependentTasks + * @return void + */ + public function addTaskToQueue(callable $task, $dependentTasks = []) + { + $dependentTasks = array_map(function (callable $task) { + return $this->createTask($task); + }, $dependentTasks); + + $task = $this->createTask($task, $dependentTasks); + $this->tasksQueue[$task->getId()] = $task; + } + + /** + * Process tasks queue + * @return int + */ + public function process() + { + $processQueue = []; + $this->internalQueueProcess($this->tasksQueue, $processQueue); + + $returnStatus = 0; + while (count($this->processManager->getProcesses()) > 0) { + foreach ($this->processManager->getProcesses() as $process) { + if ($process->isCompleted()) { + $dependedTasks = isset($this->processTaskMap[$process->getPid()]) + ? $this->processTaskMap[$process->getPid()] + : []; + + $this->processManager->delete($process); + $returnStatus |= $process->getStatus(); + + $this->internalQueueProcess(array_merge($processQueue, $dependedTasks), $processQueue); + + if (count($this->processManager->getProcesses()) >= $this->maxProcesses) { + break 1; + } + } + } + usleep(5000); + } + $this->resourceConnection->closeConnection(); + + return $returnStatus; + } + + /** + * @param ProcessTask[] $taskQueue + * @param ProcessTask[] $processQueue + * @return void + */ + private function internalQueueProcess($taskQueue, &$processQueue) + { + $processNumber = count($this->processManager->getProcesses()); + foreach ($taskQueue as $task) { + if ($processNumber >= $this->maxProcesses) { + if (!isset($processQueue[$task->getId()])) { + $processQueue[$task->getId()] = $task; + } + } else { + unset($processQueue[$task->getId()]); + $this->fork($task); + $processNumber++; + } + } + } + + /** + * @param callable $handler + * @param array $dependentTasks + * @return ProcessTask + */ + private function createTask($handler, $dependentTasks = []) + { + return $this->processTaskFactory->create(['handler' => $handler, 'dependentTasks' => $dependentTasks]); + } + + /** + * @param ProcessTask $task + * @return void + */ + private function fork(ProcessTask $task) + { + $process = $this->processManager->fork($task->getHandler()); + if ($task->getDependentTasks()) { + $pid = $process->getPid(); + foreach ($task->getDependentTasks() as $dependentTask) { + $this->processTaskMap[$pid][$dependentTask->getId()] = $dependentTask; + } + } + } +} diff --git a/app/code/Magento/Deploy/Model/ProcessTask.php b/app/code/Magento/Deploy/Model/ProcessTask.php new file mode 100644 index 0000000000000000000000000000000000000000..8db7439c3d6f0be14b98ce937f33e8e01a76c85a --- /dev/null +++ b/app/code/Magento/Deploy/Model/ProcessTask.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Deploy\Model; + +class ProcessTask +{ + /** + * @var string + */ + private $taskId; + + /** + * @var callable + */ + private $handler; + + /** + * @var array + */ + private $dependentTasks; + + /** + * @param callable $handler + * @param array $dependentTasks + */ + public function __construct($handler, array $dependentTasks = []) + { + $this->taskId = uniqid('', true); + $this->handler = $handler; + $this->dependentTasks = $dependentTasks; + } + + /** + * @return callable + */ + public function getHandler() + { + return $this->handler; + } + + /** + * @return string + */ + public function getId() + { + return $this->taskId; + } + + /** + * @return ProcessTask[] + */ + public function getDependentTasks() + { + return $this->dependentTasks; + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Console/Command/DeployStaticContentCommandTest.php b/app/code/Magento/Deploy/Test/Unit/Console/Command/DeployStaticContentCommandTest.php index 24ccf34bb894ceaf802b0031a461db612920d28c..c8fa2138e7c81fb69d562fb5ec55e049cb5deabc 100644 --- a/app/code/Magento/Deploy/Test/Unit/Console/Command/DeployStaticContentCommandTest.php +++ b/app/code/Magento/Deploy/Test/Unit/Console/Command/DeployStaticContentCommandTest.php @@ -16,7 +16,7 @@ require 'FunctionExistMock.php'; class DeployStaticContentCommandTest extends \PHPUnit_Framework_TestCase { /** - * @var \Magento\Deploy\Model\Deployer|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Deploy\Model\DeployManager|\PHPUnit_Framework_MockObject_MockObject */ private $deployer; @@ -60,7 +60,7 @@ class DeployStaticContentCommandTest extends \PHPUnit_Framework_TestCase '', false ); - $this->deployer = $this->getMock(\Magento\Deploy\Model\Deployer::class, [], [], '', false); + $this->deployer = $this->getMock(\Magento\Deploy\Model\DeployManager::class, [], [], '', false); $this->filesUtil = $this->getMock(\Magento\Framework\App\Utility\Files::class, [], [], '', false); $this->appState = $this->getMock(\Magento\Framework\App\State::class, [], [], '', false); diff --git a/app/code/Magento/Deploy/Test/Unit/Model/Deploy/LocaleDeployTest.php b/app/code/Magento/Deploy/Test/Unit/Model/Deploy/LocaleDeployTest.php new file mode 100644 index 0000000000000000000000000000000000000000..757da133ddbc3df30eac5a590c1c8dbf00fd002a --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Model/Deploy/LocaleDeployTest.php @@ -0,0 +1,214 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Deploy\Test\Unit\Model\Deploy; + +use Magento\Framework\App\Utility\Files; +use Magento\Framework\App\View\Asset\Publisher; +use Magento\Framework\Translate\Js\Config; +use Magento\Framework\View\Asset\Minification; +use Magento\Framework\View\Asset\Repository; +use Magento\Framework\View\Asset\RepositoryFactory; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class LocaleDeployTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Config + */ + private $jsTranslationMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Minification + */ + private $minificationMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|RepositoryFactory + */ + private $assetRepoFactoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\RequireJs\Model\FileManagerFactory + */ + private $fileManagerFactoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\RequireJs\ConfigFactory + */ + private $configFactoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\View\Asset\Bundle\Manager + */ + private $bundleManagerMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Files + */ + private $filesUtilMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\View\DesignInterfaceFactory + */ + private $designFactoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Locale\ResolverInterface + */ + private $localeResolverMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|OutputInterface + */ + private $outputMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|LoggerInterface + */ + private $loggerMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $assetRepoMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $assetPublisherMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $themeProviderMock; + + protected function setUp() + { + $this->outputMock = $this->getMock(OutputInterface::class, [], [], '', false); + $this->loggerMock = $this->getMock(LoggerInterface::class, [], [], '', false); + $this->filesUtilMock = $this->getMock(Files::class, [], [], '', false); + $this->assetRepoMock = $this->getMock(Repository::class, [], [], '', false); + $this->minificationMock = $this->getMock(Minification::class, [], [], '', false); + $this->jsTranslationMock = $this->getMock(Config::class, [], [], '', false); + $this->assetPublisherMock = $this->getMock(Publisher::class, [], [], '', false); + $this->assetRepoFactoryMock = $this->getMock( + RepositoryFactory::class, + ['create'], + [], + '', + false + ); + $this->fileManagerFactoryMock = $this->getMock( + \Magento\RequireJs\Model\FileManagerFactory::class, + ['create'], + [], + '', + false + ); + $this->configFactoryMock = $this->getMock( + \Magento\Framework\RequireJs\ConfigFactory::class, + ['create'], + [], + '', + false + ); + $this->bundleManagerMock = $this->getMock( + \Magento\Framework\View\Asset\Bundle\Manager::class, + [], + [], + '', + false + ); + $this->themeProviderMock = $this->getMock( + \Magento\Framework\View\Design\Theme\ThemeProviderInterface::class, + [], + [], + '', + false + ); + $this->designFactoryMock = $this->getMock( + \Magento\Framework\View\DesignInterfaceFactory::class, + ['create'], + [], + '', + false + ); + $this->localeResolverMock = $this->getMock( + \Magento\Framework\Locale\ResolverInterface::class, + [], + [], + '', + false + ); + } + + public function testDeploy() + { + $area = 'adminhtml'; + $themePath = '/theme/path'; + $locale = 'en_US'; + + $designMock = $this->getMock(\Magento\Framework\View\DesignInterface::class, [], [], '', false); + $assetRepoMock = $this->getMock(Repository::class, [], [], '', false); + $requireJsConfigMock = $this->getMock(\Magento\Framework\RequireJs\Config::class, [], [], '', false); + $fileManagerMock = $this->getMock(\Magento\RequireJs\Model\FileManager::class, [], [], '', false); + + $model = $this->getModel([\Magento\Deploy\Console\Command\DeployStaticOptionsInterface::NO_JAVASCRIPT => 0]); + + $this->localeResolverMock->expects($this->once())->method('setLocale')->with($locale); + $this->designFactoryMock->expects($this->once())->method('create')->willReturn($designMock); + $designMock->expects($this->once())->method('setDesignTheme')->with($themePath, $area)->willReturnSelf(); + $this->assetRepoFactoryMock->expects($this->once())->method('create')->with(['design' => $designMock]) + ->willReturn($assetRepoMock); + $this->configFactoryMock->expects($this->once())->method('create')->willReturn($requireJsConfigMock); + $this->fileManagerFactoryMock->expects($this->once())->method('create')->willReturn($fileManagerMock); + + $fileManagerMock->expects($this->once())->method('createRequireJsConfigAsset')->willReturnSelf(); + $this->filesUtilMock->expects($this->once())->method('getStaticPreProcessingFiles')->willReturn([]); + $this->filesUtilMock->expects($this->once())->method('getStaticLibraryFiles')->willReturn([]); + + $this->jsTranslationMock->expects($this->once())->method('dictionaryEnabled')->willReturn(false); + $this->minificationMock->expects($this->once())->method('isEnabled')->with('js')->willReturn(true); + $fileManagerMock->expects($this->once())->method('createMinResolverAsset')->willReturnSelf(); + + $this->bundleManagerMock->expects($this->once())->method('flush'); + + $this->assertEquals( + \Magento\Framework\Console\Cli::RETURN_SUCCESS, + $model->deploy($area, $themePath, $locale) + ); + } + + /** + * @param array $options + * @return \Magento\Deploy\Model\Deploy\LocaleDeploy + */ + private function getModel($options = []) + { + return new \Magento\Deploy\Model\Deploy\LocaleDeploy( + $this->outputMock, + $this->jsTranslationMock, + $this->minificationMock, + $this->assetRepoMock, + $this->assetRepoFactoryMock, + $this->fileManagerFactoryMock, + $this->configFactoryMock, + $this->assetPublisherMock, + $this->bundleManagerMock, + $this->themeProviderMock, + $this->loggerMock, + $this->filesUtilMock, + $this->designFactoryMock, + $this->localeResolverMock, + [], + $options + ); + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Model/Deploy/LocaleQuickDeployTest.php b/app/code/Magento/Deploy/Test/Unit/Model/Deploy/LocaleQuickDeployTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0c7d459964b7d028e081b85440a58b991acb5661 --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Model/Deploy/LocaleQuickDeployTest.php @@ -0,0 +1,114 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Deploy\Test\Unit\Model\Deploy; + +use Magento\Deploy\Model\Deploy\DeployInterface; +use Magento\Deploy\Model\Deploy\LocaleQuickDeploy; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Symfony\Component\Console\Output\OutputInterface; +use Magento\Deploy\Console\Command\DeployStaticOptionsInterface as Options; +use \Magento\Framework\RequireJs\Config as RequireJsConfig; + +class LocaleQuickDeployTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var OutputInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $outputMock; + + /** + * @var WriteInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $staticDirectoryMock; + + protected function setUp() + { + $this->outputMock = $this->getMockBuilder(OutputInterface::class) + ->setMethods(['writeln']) + ->getMockForAbstractClass(); + + $this->staticDirectoryMock = $this->getMockBuilder(WriteInterface::class) + ->setMethods(['createSymlink', 'getAbsolutePath', 'getRelativePath', 'copyFile', 'readRecursively']) + ->getMockForAbstractClass(); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Deploy base locale must be set for Quick Deploy + */ + public function testDeployWithoutBaseLocale() + { + $this->getModel()->deploy('adminhtml', 'Magento/backend', 'en_US'); + } + + public function testDeployWithSymlinkStrategy() + { + $area = 'adminhtml'; + $themePath = 'Magento/backend'; + $locale = 'uk_UA'; + $baseLocal = 'en_US'; + + $this->staticDirectoryMock->expects(self::exactly(2)) + ->method('createSymlink') + ->withConsecutive( + ['adminhtml/Magento/backend/en_US', 'adminhtml/Magento/backend/uk_UA'], + ['_requirejs/adminhtml/Magento/backend/en_US', '_requirejs/adminhtml/Magento/backend/uk_UA'] + ); + + $model = $this->getModel([ + DeployInterface::DEPLOY_BASE_LOCALE => $baseLocal, + Options::SYMLINK_LOCALE => 1, + ]); + $model->deploy($area, $themePath, $locale); + } + + public function testDeployWithCopyStrategy() + { + + $area = 'adminhtml'; + $themePath = 'Magento/backend'; + $locale = 'uk_UA'; + $baseLocal = 'en_US'; + + $this->staticDirectoryMock->expects(self::never())->method('createSymlink'); + $this->staticDirectoryMock->expects(self::exactly(2))->method('readRecursively')->willReturnMap([ + ['adminhtml/Magento/backend/en_US', [$baseLocal . 'file1', $baseLocal . 'dir']], + [RequireJsConfig::DIR_NAME . '/adminhtml/Magento/backend/en_US', [$baseLocal . 'file2']] + ]); + $this->staticDirectoryMock->expects(self::exactly(3))->method('isFile')->willReturnMap([ + [$baseLocal . 'file1', true], + [$baseLocal . 'dir', false], + [$baseLocal . 'file2', true], + ]); + $this->staticDirectoryMock->expects(self::exactly(2))->method('copyFile')->withConsecutive( + [$baseLocal . 'file1', $locale . 'file1', null], + [$baseLocal . 'file2', $locale . 'file2', null] + ); + + $model = $this->getModel([ + DeployInterface::DEPLOY_BASE_LOCALE => $baseLocal, + Options::SYMLINK_LOCALE => 0, + ]); + $model->deploy($area, $themePath, $locale); + } + + /** + * @param array $options + * @return LocaleQuickDeploy + */ + private function getModel($options = []) + { + return (new ObjectManager($this))->getObject( + LocaleQuickDeploy::class, + [ + 'output' => $this->outputMock, + 'staticDirectory' => $this->staticDirectoryMock, + 'options' => $options + ] + ); + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Model/Deploy/TemplateMinifierTest.php b/app/code/Magento/Deploy/Test/Unit/Model/Deploy/TemplateMinifierTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e6859faeca88a34692cf8b8c71f2532757207147 --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Model/Deploy/TemplateMinifierTest.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Deploy\Test\Unit\Model\Deploy; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class TemplateMinifierTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Deploy\Model\Deploy\TemplateMinifier + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\App\Utility\Files + */ + private $filesUtilsMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\View\Template\Html\MinifierInterface + */ + private $minifierMock; + + protected function setUp() + { + $this->minifierMock = $this->getMock( + \Magento\Framework\View\Template\Html\MinifierInterface::class, + [], + [], + '', + false + ); + $this->filesUtilsMock = $this->getMock(\Magento\Framework\App\Utility\Files::class, [], [], '', false); + + $this->model = new \Magento\Deploy\Model\Deploy\TemplateMinifier( + $this->filesUtilsMock, + $this->minifierMock + ); + } + + public function testMinifyTemplates() + { + $templateMock = "template.phtml"; + $templatesMock = [$templateMock]; + + $this->filesUtilsMock->expects($this->once())->method('getPhtmlFiles')->with(false, false) + ->willReturn($templatesMock); + $this->minifierMock->expects($this->once())->method('minify')->with($templateMock); + + self::assertEquals(1, $this->model->minifyTemplates()); + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Model/DeployManagerTest.php b/app/code/Magento/Deploy/Test/Unit/Model/DeployManagerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..b9a9f6049f0210ca3ad19a3ced6cc042b178e312 --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Model/DeployManagerTest.php @@ -0,0 +1,168 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Deploy\Test\Unit\Model; + +use Magento\Deploy\Console\Command\DeployStaticOptionsInterface as Options; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class DeployManagerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Deploy\Model\DeployStrategyProviderFactory + */ + private $deployStrategyProviderFactoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Symfony\Component\Console\Output\OutputInterface + */ + private $outputMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\App\View\Deployment\Version\StorageInterface + */ + private $versionStorageMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Deploy\Model\Deploy\TemplateMinifier + */ + private $minifierTemplateMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Deploy\Model\ProcessQueueManagerFactory + */ + private $processQueueManagerFactoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\App\State + */ + private $stateMock; + + protected function setUp() + { + $this->deployStrategyProviderFactoryMock = $this->getMock( + \Magento\Deploy\Model\DeployStrategyProviderFactory::class, + ['create'], + [], + '', + false + ); + $this->versionStorageMock = $this->getMock( + \Magento\Framework\App\View\Deployment\Version\StorageInterface::class, + [], + [], + '', + false + ); + $this->minifierTemplateMock = $this->getMock( + \Magento\Deploy\Model\Deploy\TemplateMinifier::class, + [], + [], + '', + false + ); + $this->processQueueManagerFactoryMock = $this->getMock( + \Magento\Deploy\Model\ProcessQueueManagerFactory::class, + [], + [], + '', + false + ); + $this->stateMock = $this->getMockBuilder(\Magento\Framework\App\State::class) + ->disableOriginalConstructor() + ->getMock(); + $this->outputMock = $this->getMock(\Symfony\Component\Console\Output\OutputInterface::class, [], [], '', false); + } + + public function testSaveDeployedVersion() + { + $version = (new \DateTime())->getTimestamp(); + $this->outputMock->expects($this->once())->method('writeln')->with("New version of deployed files: {$version}"); + $this->versionStorageMock->expects($this->once())->method('save')->with($version); + + $this->assertEquals( + \Magento\Framework\Console\Cli::RETURN_SUCCESS, + $this->getModel([Options::NO_HTML_MINIFY => true])->deploy() + ); + } + + public function testSaveDeployedVersionDryRun() + { + $options = [Options::DRY_RUN => true, Options::NO_HTML_MINIFY => true]; + + $this->outputMock->expects(self::once())->method('writeln')->with( + 'Dry run. Nothing will be recorded to the target directory.' + ); + $this->versionStorageMock->expects($this->never())->method('save'); + + $this->getModel($options)->deploy(); + } + + public function testMinifyTemplates() + { + $this->minifierTemplateMock->expects($this->once())->method('minifyTemplates')->willReturn(2); + $this->outputMock->expects($this->atLeastOnce())->method('writeln')->withConsecutive( + ["=== Minify templates ==="], + ["\nSuccessful: 2 files modified\n---\n"] + ); + + $this->getModel([Options::NO_HTML_MINIFY => false])->deploy(); + } + + public function testMinifyTemplatesNoHtmlMinify() + { + $version = (new \DateTime())->getTimestamp(); + $this->outputMock->expects($this->once())->method('writeln')->with("New version of deployed files: {$version}"); + $this->versionStorageMock->expects($this->once())->method('save')->with($version); + + $this->getModel([Options::NO_HTML_MINIFY => true])->deploy(); + } + + public function testDeploy() + { + $area = 'frontend'; + $themePath = 'themepath'; + $locale = 'en_US'; + $options = [Options::NO_HTML_MINIFY => true]; + $strategyProviderMock = $this->getMock(\Magento\Deploy\Model\DeployStrategyProvider::class, [], [], '', false); + $deployStrategyMock = $this->getMock(\Magento\Deploy\Model\Deploy\DeployInterface::class, [], [], '', false); + + $model = $this->getModel($options); + $model->addPack($area, $themePath, $locale); + $this->deployStrategyProviderFactoryMock->expects($this->once())->method('create')->with( + ['output' => $this->outputMock, 'options' => $options] + )->willReturn($strategyProviderMock); + $strategyProviderMock->expects($this->once())->method('getDeployStrategies')->with($area, $themePath, [$locale]) + ->willReturn([$locale => $deployStrategyMock]); + $this->stateMock->expects(self::once())->method('emulateAreaCode') + ->with($area, [$deployStrategyMock, 'deploy'], [$area, $themePath, $locale]) + ->willReturn(\Magento\Framework\Console\Cli::RETURN_SUCCESS); + + $version = (new \DateTime())->getTimestamp(); + $this->outputMock->expects(self::once())->method('writeln')->with("New version of deployed files: {$version}"); + $this->versionStorageMock->expects($this->once())->method('save')->with($version); + + $this->assertEquals(\Magento\Framework\Console\Cli::RETURN_SUCCESS, $model->deploy()); + } + + /** + * @param array $options + * @return \Magento\Deploy\Model\DeployManager + */ + private function getModel(array $options) + { + return new \Magento\Deploy\Model\DeployManager( + $this->outputMock, + $this->versionStorageMock, + $this->deployStrategyProviderFactoryMock, + $this->processQueueManagerFactoryMock, + $this->minifierTemplateMock, + $this->stateMock, + $options + ); + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Model/DeployStrategyFactoryTest.php b/app/code/Magento/Deploy/Test/Unit/Model/DeployStrategyFactoryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2af443ef061e887cf6181adb2ff945812660f099 --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Model/DeployStrategyFactoryTest.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Deploy\Test\Unit\Model; + +use Magento\Deploy\Model\Deploy\LocaleDeploy; +use Magento\Deploy\Model\DeployStrategyFactory; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +class DeployStrategyFactoryTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $objectManagerMock; + + /** + * @var DeployStrategyFactory + */ + private $unit; + + protected function setUp() + { + $this->objectManagerMock = $this->getMock(ObjectManagerInterface::class); + + $this->unit = (new ObjectManager($this))->getObject( + DeployStrategyFactory::class, + [ + 'objectManager' => $this->objectManagerMock, + ] + ); + } + + /** + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage Wrong deploy strategy type: wrong-type + */ + public function testCreateWithWrongStrategyType() + { + $this->unit->create('wrong-type'); + } + + public function testCreate() + { + $this->objectManagerMock->expects(self::once())->method('create') + ->with(LocaleDeploy::class, ['arg1' => 1]); + + $this->unit->create(DeployStrategyFactory::DEPLOY_STRATEGY_STANDARD, ['arg1' => 1]); + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Model/ProcessQueueManagerTest.php b/app/code/Magento/Deploy/Test/Unit/Model/ProcessQueueManagerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e77068755036ed4983b920de4f49173a1337b753 --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Model/ProcessQueueManagerTest.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Deploy\Test\Unit\Model; + +use Magento\Deploy\Model\ProcessManager; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Deploy\Model\ProcessTaskFactory; +use Magento\Deploy\Model\ProcessTask; + +class ProcessQueueManagerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Deploy\Model\ProcessQueueManager + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Deploy\Model\ProcessManager + */ + private $processManagerMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\App\ResourceConnection + */ + private $resourceConnectionMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|ProcessTaskFactory + */ + private $processTaskFactoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|ProcessTask + */ + private $processTaskMock; + + protected function setUp() + { + $this->processManagerMock = $this->getMock(ProcessManager::class, [], [], '', false); + $this->resourceConnectionMock = $this->getMock(ResourceConnection::class, [], [], '', false); + $this->processTaskFactoryMock = $this->getMock(ProcessTaskFactory::class, ['create'], [], '', false); + $this->processTaskMock = $this->getMock(ProcessTask::class, [], [], '', false); + $this->processTaskFactoryMock->expects($this->any())->method('create')->willReturn($this->processTaskMock); + $this->model = (new ObjectManager($this))->getObject( + \Magento\Deploy\Model\ProcessQueueManager::class, + [ + 'processManager' => $this->processManagerMock, + 'resourceConnection' => $this->resourceConnectionMock, + 'processTaskFactory' => $this->processTaskFactoryMock + ] + ); + } + + public function testProcess() + { + $callableMock = function () { + return true; + }; + $this->processTaskMock->expects($this->any())->method('getHandler')->willReturn($callableMock); + + $processMock = $this->getMock(\Magento\Deploy\Model\Process::class, [], [], '', false); + + $this->model->addTaskToQueue($callableMock, []); + $this->processManagerMock->expects($this->atLeastOnce())->method('getProcesses')->willReturnOnConsecutiveCalls( + [$processMock], + [$processMock], + [$processMock], + [$processMock], + [$processMock], + [] + ); + $processMock->expects($this->once())->method('isCompleted')->willReturn(true); + $processMock->expects($this->atLeastOnce())->method('getPid')->willReturn(42); + $processMock->expects($this->once())->method('getStatus')->willReturn(0); + $this->processManagerMock->expects($this->once())->method('delete')->with($processMock); + + $this->resourceConnectionMock->expects(self::once())->method('closeConnection'); + + $this->assertEquals(0, $this->model->process()); + } +} diff --git a/app/code/Magento/Deploy/composer.json b/app/code/Magento/Deploy/composer.json index 05db53f039027e2ca8e62f490005a782e0dfc088..856e0d8b3e542a37cdd7fcf7554b9e4e158de245 100644 --- a/app/code/Magento/Deploy/composer.json +++ b/app/code/Magento/Deploy/composer.json @@ -5,7 +5,6 @@ "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", "magento/framework": "100.2.*", "magento/module-store": "100.2.*", - "magento/module-theme": "100.2.*", "magento/module-require-js": "100.2.*", "magento/module-user": "100.2.*" }, diff --git a/app/code/Magento/Deploy/etc/di.xml b/app/code/Magento/Deploy/etc/di.xml index e1a0295a0fe32b8a916755f717ad54876c474558..52c880c28d0a71ad7eaeb34b6c2096c6c7b5060a 100644 --- a/app/code/Magento/Deploy/etc/di.xml +++ b/app/code/Magento/Deploy/etc/di.xml @@ -13,6 +13,13 @@ </argument> </arguments> </type> + <type name="Magento\Deploy\Model\Deploy\LocaleDeploy"> + <arguments> + <argument name="alternativeSources" xsi:type="array"> + <item name="css" xsi:type="object">AlternativeSourceProcessors</item> + </argument> + </arguments> + </type> <type name="Magento\Framework\Console\CommandListInterface"> <arguments> <argument name="commands" xsi:type="array"> diff --git a/app/code/Magento/Email/Model/Template/Css/Processor.php b/app/code/Magento/Email/Model/Template/Css/Processor.php new file mode 100644 index 0000000000000000000000000000000000000000..ae7d083750863d2d7dbd5869341bbee772544421 --- /dev/null +++ b/app/code/Magento/Email/Model/Template/Css/Processor.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Email\Model\Template\Css; + +use Magento\Framework\View\Asset\NotationResolver\Variable; +use Magento\Framework\View\Asset\Repository; + +class Processor +{ + /** + * @var Repository + */ + private $assetRepository; + + /** + * @param Repository $assetRepository + */ + public function __construct(Repository $assetRepository) + { + $this->assetRepository = $assetRepository; + } + + /** + * Process css placeholders + * + * @param string $css + * @return string + */ + public function process($css) + { + $matches = []; + if (preg_match_all(Variable::VAR_REGEX, $css, $matches, PREG_SET_ORDER)) { + $replacements = []; + foreach ($matches as $match) { + if (!isset($replacements[$match[0]])) { + $replacements[$match[0]] = $this->getPlaceholderValue($match[1]); + } + } + $css = str_replace(array_keys($replacements), $replacements, $css); + } + return $css; + } + + /** + * Retrieve placeholder value + * + * @param string $placeholder + * @return string + */ + private function getPlaceholderValue($placeholder) + { + /** @var \Magento\Framework\View\Asset\File\FallbackContext $context */ + $context = $this->assetRepository->getStaticViewFileContext(); + + switch ($placeholder) { + case 'base_url_path': + return $context->getBaseUrl(); + case 'locale': + return $context->getLocale(); + default: + return ''; + } + } +} diff --git a/app/code/Magento/Email/Model/Template/Filter.php b/app/code/Magento/Email/Model/Template/Filter.php index 2bb9fc742f258090d1cda46b538053e00fb896e8..d9409d62f159cfd7c2ab1e6c32aae6d07a33b8fc 100644 --- a/app/code/Magento/Email/Model/Template/Filter.php +++ b/app/code/Magento/Email/Model/Template/Filter.php @@ -5,6 +5,9 @@ */ namespace Magento\Email\Model\Template; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\View\Asset\ContentProcessorException; use Magento\Framework\View\Asset\ContentProcessorInterface; @@ -153,6 +156,16 @@ class Filter extends \Magento\Framework\Filter\Template */ protected $configVariables; + /** + * @var \Magento\Email\Model\Template\Css\Processor + */ + private $cssProcessor; + + /** + * @var ReadInterface + */ + private $pubDirectory; + /** * @param \Magento\Framework\Stdlib\StringUtils $string * @param \Psr\Log\LoggerInterface $logger @@ -203,6 +216,31 @@ class Filter extends \Magento\Framework\Filter\Template parent::__construct($string, $variables); } + /** + * @deprecated + * @return Css\Processor + */ + private function getCssProcessor() + { + if (!$this->cssProcessor) { + $this->cssProcessor = ObjectManager::getInstance()->get(Css\Processor::class); + } + return $this->cssProcessor; + } + + /** + * @deprecated + * @param string $dirType + * @return ReadInterface + */ + private function getPubDirectory($dirType) + { + if (!$this->pubDirectory) { + $this->pubDirectory = ObjectManager::getInstance()->get(Filesystem::class)->getDirectoryRead($dirType); + } + return $this->pubDirectory; + } + /** * Set use absolute links flag * @@ -788,7 +826,9 @@ class Filter extends \Magento\Framework\Filter\Template return '/* ' . __('"file" parameter must be specified') . ' */'; } - $css = $this->getCssFilesContent([$params['file']]); + $css = $this->getCssProcessor()->process( + $this->getCssFilesContent([$params['file']]) + ); if (strpos($css, ContentProcessorInterface::ERROR_MESSAGE_PREFIX) !== false) { // Return compilation error wrapped in CSS comment @@ -889,7 +929,12 @@ class Filter extends \Magento\Framework\Filter\Template try { foreach ($files as $file) { $asset = $this->_assetRepo->createAsset($file, $designParams); - $css .= $asset->getContent(); + $pubDirectory = $this->getPubDirectory($asset->getContext()->getBaseDirType()); + if ($pubDirectory->isExist($asset->getPath())) { + $css .= $pubDirectory->readFile($asset->getPath()); + } else { + $css .= $asset->getContent(); + } } } catch (ContentProcessorException $exception) { $css = $exception->getMessage(); @@ -914,6 +959,8 @@ class Filter extends \Magento\Framework\Filter\Template $cssToInline = $this->getCssFilesContent( $this->getInlineCssFiles() ); + $cssToInline = $this->getCssProcessor()->process($cssToInline); + // Only run Emogrify if HTML and CSS contain content if ($html && $cssToInline) { try { diff --git a/app/code/Magento/Email/Test/Unit/Model/Template/Css/ProcessorTest.php b/app/code/Magento/Email/Test/Unit/Model/Template/Css/ProcessorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..fac9ba0f1b5aa902a22f08141e5f0c824a1a1382 --- /dev/null +++ b/app/code/Magento/Email/Test/Unit/Model/Template/Css/ProcessorTest.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Email\Test\Unit\Model\Template\Css; + +use Magento\Email\Model\Template\Css\Processor; +use Magento\Framework\View\Asset\File\FallbackContext; +use Magento\Framework\View\Asset\Repository; + +class ProcessorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var Processor + */ + protected $processor; + + /** + * @var Repository|\PHPUnit_Framework_MockObject_MockObject + */ + protected $assetRepository; + + /** + * @var FallbackContext|\PHPUnit_Framework_MockObject_MockObject + */ + protected $fallbackContext; + + public function setUp() + { + $this->assetRepository = $this->getMockBuilder(Repository::class) + ->disableOriginalConstructor() + ->getMock(); + $this->fallbackContext = $this->getMockBuilder(FallbackContext::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->processor = new Processor($this->assetRepository); + } + + public function testProcess() + { + $url = 'http://magento.local/pub/static/'; + $locale = 'en_US'; + $css = '@import url("{{base_url_path}}frontend/_view/{{locale}}/css/email.css");'; + $expectedCss = '@import url("' . $url . 'frontend/_view/' . $locale . '/css/email.css");'; + + $this->assetRepository->expects($this->exactly(2)) + ->method('getStaticViewFileContext') + ->willReturn($this->fallbackContext); + $this->fallbackContext->expects($this->once()) + ->method('getBaseUrl') + ->willReturn($url); + $this->fallbackContext->expects($this->once()) + ->method('getLocale') + ->willReturn($locale); + $this->assertEquals($expectedCss, $this->processor->process($css)); + } +} 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 84e87e9e8154156b7323e1cbfd5d6d11ebdaab39..bcfd95d897d298faf58543b08ad3385e71f66476 100644 --- a/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php @@ -5,6 +5,13 @@ */ namespace Magento\Email\Test\Unit\Model\Template; +use Magento\Email\Model\Template\Css\Processor; +use Magento\Email\Model\Template\Filter; +use Magento\Framework\App\Area; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\Framework\View\Asset\File\FallbackContext; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -94,7 +101,6 @@ class FilterTest extends \PHPUnit_Framework_TestCase $this->escaper = $this->getMockBuilder(\Magento\Framework\Escaper::class) ->disableOriginalConstructor() - ->enableProxyingToOriginalMethods() ->getMock(); $this->assetRepo = $this->getMockBuilder(\Magento\Framework\View\Asset\Repository::class) @@ -138,7 +144,7 @@ class FilterTest extends \PHPUnit_Framework_TestCase /** * @param array|null $mockedMethods Methods to mock - * @return \Magento\Email\Model\Template\Filter|\PHPUnit_Framework_MockObject_MockObject + * @return Filter|\PHPUnit_Framework_MockObject_MockObject */ protected function getModel($mockedMethods = null) { @@ -252,13 +258,23 @@ class FilterTest extends \PHPUnit_Framework_TestCase public function testApplyInlineCss($html, $css, $expectedResults) { $filter = $this->getModel(['getCssFilesContent']); + $cssProcessor = $this->getMockBuilder(Processor::class) + ->disableOriginalConstructor() + ->getMock(); + $reflectionClass = new \ReflectionClass(Filter::class); + $reflectionProperty = $reflectionClass->getProperty('cssProcessor'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($filter, $cssProcessor); + $cssProcessor->expects($this->any()) + ->method('process') + ->willReturnArgument(0); $filter->expects($this->exactly(count($expectedResults))) ->method('getCssFilesContent') ->will($this->returnValue($css)); $designParams = [ - 'area' => \Magento\Framework\App\Area::AREA_FRONTEND, + 'area' => Area::AREA_FRONTEND, 'theme' => 'themeId', 'locale' => 'localeId', ]; @@ -269,6 +285,60 @@ class FilterTest extends \PHPUnit_Framework_TestCase } } + public function testGetCssFilesContent() + { + $file = 'css/email.css'; + $path = Area::AREA_FRONTEND . '/themeId/localeId'; + $css = 'p{color:black}'; + $designParams = [ + 'area' => Area::AREA_FRONTEND, + 'theme' => 'themeId', + 'locale' => 'localeId', + ]; + $filter = $this->getModel(); + + $asset = $this->getMockBuilder(\Magento\Framework\View\Asset\File::class) + ->disableOriginalConstructor() + ->getMock(); + + $fallbackContext = $this->getMockBuilder(FallbackContext::class) + ->disableOriginalConstructor() + ->getMock(); + $fallbackContext->expects($this->once()) + ->method('getBaseDirType') + ->willReturn(DirectoryList::STATIC_VIEW); + $asset->expects($this->atLeastOnce()) + ->method('getContext') + ->willReturn($fallbackContext); + + $asset->expects($this->atLeastOnce()) + ->method('getPath') + ->willReturn($path . DIRECTORY_SEPARATOR . $file); + $this->assetRepo->expects($this->once()) + ->method('createAsset') + ->with($file, $designParams) + ->willReturn($asset); + + $pubDirectory = $this->getMockBuilder(ReadInterface::class) + ->getMockForAbstractClass(); + $reflectionClass = new \ReflectionClass(Filter::class); + $reflectionProperty = $reflectionClass->getProperty('pubDirectory'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($filter, $pubDirectory); + $pubDirectory->expects($this->once()) + ->method('isExist') + ->with($path . DIRECTORY_SEPARATOR . $file) + ->willReturn(true); + $pubDirectory->expects($this->once()) + ->method('readFile') + ->with($path . DIRECTORY_SEPARATOR . $file) + ->willReturn($css); + + $filter->setDesignParams($designParams); + + $this->assertEquals($css, $filter->getCssFilesContent([$file])); + } + /** * @return array */ @@ -301,7 +371,19 @@ class FilterTest extends \PHPUnit_Framework_TestCase */ public function testApplyInlineCssThrowsExceptionWhenDesignParamsNotSet() { - $this->getModel()->applyInlineCss('test'); + $filter = $this->getModel(); + $cssProcessor = $this->getMockBuilder(Processor::class) + ->disableOriginalConstructor() + ->getMock(); + $reflectionClass = new \ReflectionClass(Filter::class); + $reflectionProperty = $reflectionClass->getProperty('cssProcessor'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($filter, $cssProcessor); + $cssProcessor->expects($this->any()) + ->method('process') + ->willReturnArgument(0); + + $filter->applyInlineCss('test'); } /** @@ -348,7 +430,10 @@ class FilterTest extends \PHPUnit_Framework_TestCase $construction = ["{{config path={$path}}}", 'config', " path={$path}"]; $scopeConfigValue = 'value'; - $storeMock = $this->getMock(\Magento\Store\Api\Data\StoreInterface::class, [], [], '', false); + $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeManager->expects($this->once())->method('getStore')->willReturn($storeMock); $storeMock->expects($this->once())->method('getId')->willReturn(1); @@ -369,7 +454,9 @@ class FilterTest extends \PHPUnit_Framework_TestCase $construction = ["{{config path={$path}}}", 'config', " path={$path}"]; $scopeConfigValue = ''; - $storeMock = $this->getMock(\Magento\Store\Api\Data\StoreInterface::class, [], [], '', false); + $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) + ->disableOriginalConstructor() + ->getMock(); $this->storeManager->expects($this->once())->method('getStore')->willReturn($storeMock); $storeMock->expects($this->once())->method('getId')->willReturn(1); diff --git a/app/code/Magento/Multishipping/Block/Checkout/Billing.php b/app/code/Magento/Multishipping/Block/Checkout/Billing.php index f34ed5fb2671a649f7f8abc2d201b9ffc19dc0b0..34570e12a29efc0071cad327af15f50d64686f35 100644 --- a/app/code/Magento/Multishipping/Block/Checkout/Billing.php +++ b/app/code/Magento/Multishipping/Block/Checkout/Billing.php @@ -35,6 +35,7 @@ class Billing extends \Magento\Payment\Block\Form\Container * @param \Magento\Checkout\Model\Session $checkoutSession * @param \Magento\Payment\Model\Method\SpecificationInterface $paymentSpecification * @param array $data + * @param array $additionalChecks */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -43,12 +44,13 @@ class Billing extends \Magento\Payment\Block\Form\Container \Magento\Multishipping\Model\Checkout\Type\Multishipping $multishipping, \Magento\Checkout\Model\Session $checkoutSession, \Magento\Payment\Model\Method\SpecificationInterface $paymentSpecification, - array $data = [] + array $data = [], + array $additionalChecks = [] ) { $this->_multishipping = $multishipping; $this->_checkoutSession = $checkoutSession; $this->paymentSpecification = $paymentSpecification; - parent::__construct($context, $paymentHelper, $methodSpecificationFactory, $data); + parent::__construct($context, $paymentHelper, $methodSpecificationFactory, $data, $additionalChecks); $this->_isScopePrivate = true; } diff --git a/app/code/Magento/Payment/Api/Data/PaymentMethodInterface.php b/app/code/Magento/Payment/Api/Data/PaymentMethodInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..881595f60e5415fdeacf3968ccfd8f01b8539ec1 --- /dev/null +++ b/app/code/Magento/Payment/Api/Data/PaymentMethodInterface.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Payment\Api\Data; + +/** + * Payment method interface. + * + * @api + */ +interface PaymentMethodInterface +{ + /** + * Get code. + * + * @return string + */ + public function getCode(); + + /** + * Get title. + * + * @return string + */ + public function getTitle(); + + /** + * Get store id. + * + * @return int + */ + public function getStoreId(); + + /** + * Get is active. + * + * @return bool + * @SuppressWarnings(PHPMD.BooleanGetMethodName) + */ + public function getIsActive(); +} diff --git a/app/code/Magento/Payment/Api/PaymentMethodListInterface.php b/app/code/Magento/Payment/Api/PaymentMethodListInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..faf231562d9ed202073161a5dd7ac927e353a0ae --- /dev/null +++ b/app/code/Magento/Payment/Api/PaymentMethodListInterface.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Payment\Api; + +/** + * Payment method list interface. + * + * @api + */ +interface PaymentMethodListInterface +{ + /** + * Get list of payment methods. + * + * @param int $storeId + * @return \Magento\Payment\Api\Data\PaymentMethodInterface[] + */ + public function getList($storeId); + + /** + * Get list of active payment methods. + * + * @param int $storeId + * @return \Magento\Payment\Api\Data\PaymentMethodInterface[] + */ + public function getActiveList($storeId); +} diff --git a/app/code/Magento/Payment/Block/Form/Container.php b/app/code/Magento/Payment/Block/Form/Container.php index cdcd4137159a1994face7ed1ca5cd89e3c0ad3c2..d91c0e3dc39a454d39d6882da41374f4da71383f 100644 --- a/app/code/Magento/Payment/Block/Form/Container.php +++ b/app/code/Magento/Payment/Block/Form/Container.php @@ -5,6 +5,7 @@ */ namespace Magento\Payment\Block\Form; +use Magento\Framework\App\ObjectManager; use Magento\Payment\Model\Method\AbstractMethod; /** @@ -24,20 +25,38 @@ class Container extends \Magento\Framework\View\Element\Template /** @var \Magento\Payment\Model\Checks\SpecificationFactory */ protected $methodSpecificationFactory; + /** + * @var \Magento\Payment\Api\PaymentMethodListInterface + */ + private $paymentMethodList; + + /** + * @var \Magento\Payment\Model\Method\InstanceFactory + */ + private $paymentMethodInstanceFactory; + + /** + * @var array + */ + protected $additionalChecks; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Payment\Helper\Data $paymentHelper * @param \Magento\Payment\Model\Checks\SpecificationFactory $methodSpecificationFactory * @param array $data + * @param array $additionalChecks */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Magento\Payment\Helper\Data $paymentHelper, \Magento\Payment\Model\Checks\SpecificationFactory $methodSpecificationFactory, - array $data = [] + array $data = [], + array $additionalChecks = [] ) { $this->_paymentHelper = $paymentHelper; $this->methodSpecificationFactory = $methodSpecificationFactory; + $this->additionalChecks = $additionalChecks; parent::__construct($context, $data); } @@ -69,13 +88,17 @@ class Container extends \Magento\Framework\View\Element\Template */ protected function _canUseMethod($method) { - return $this->methodSpecificationFactory->create( + $checks = array_merge( [ AbstractMethod::CHECK_USE_FOR_COUNTRY, AbstractMethod::CHECK_USE_FOR_CURRENCY, AbstractMethod::CHECK_ORDER_TOTAL_MIN_MAX, - ] - )->isApplicable( + AbstractMethod::CHECK_ZERO_TOTAL + ], + $this->additionalChecks + ); + + return $this->methodSpecificationFactory->create($checks)->isApplicable( $method, $this->getQuote() ); @@ -124,11 +147,11 @@ class Container extends \Magento\Framework\View\Element\Template $quote = $this->getQuote(); $store = $quote ? $quote->getStoreId() : null; $methods = []; - $specification = $this->methodSpecificationFactory->create([AbstractMethod::CHECK_ZERO_TOTAL]); - foreach ($this->_paymentHelper->getStoreMethods($store, $quote) as $method) { - if ($this->_canUseMethod($method) && $specification->isApplicable($method, $this->getQuote())) { - $this->_assignMethod($method); - $methods[] = $method; + foreach ($this->getPaymentMethodList()->getActiveList($store) as $method) { + $methodInstance = $this->getPaymentMethodInstanceFactory()->create($method); + if ($methodInstance->isAvailable($quote) && $this->_canUseMethod($methodInstance)) { + $this->_assignMethod($methodInstance); + $methods[] = $methodInstance; } } $this->setData('methods', $methods); @@ -150,4 +173,36 @@ class Container extends \Magento\Framework\View\Element\Template } return false; } + + /** + * Get payment method list. + * + * @return \Magento\Payment\Api\PaymentMethodListInterface + * @deprecated + */ + private function getPaymentMethodList() + { + if ($this->paymentMethodList === null) { + $this->paymentMethodList = ObjectManager::getInstance()->get( + \Magento\Payment\Api\PaymentMethodListInterface::class + ); + } + return $this->paymentMethodList; + } + + /** + * Get payment method instance factory. + * + * @return \Magento\Payment\Model\Method\InstanceFactory + * @deprecated + */ + private function getPaymentMethodInstanceFactory() + { + if ($this->paymentMethodInstanceFactory === null) { + $this->paymentMethodInstanceFactory = ObjectManager::getInstance()->get( + \Magento\Payment\Model\Method\InstanceFactory::class + ); + } + return $this->paymentMethodInstanceFactory; + } } diff --git a/app/code/Magento/Payment/Helper/Data.php b/app/code/Magento/Payment/Helper/Data.php index 78cde93ba6717a85a04feb6a2bec21090d551695..0c5518a5c4c9f98e0e9f615b8df78bc85163abc9 100644 --- a/app/code/Magento/Payment/Helper/Data.php +++ b/app/code/Magento/Payment/Helper/Data.php @@ -118,6 +118,7 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper * @param null|string|bool|int $store * @param Quote|null $quote * @return AbstractMethod[] + * @deprecated */ public function getStoreMethods($store = null, $quote = null) { diff --git a/app/code/Magento/Payment/Model/Method/InstanceFactory.php b/app/code/Magento/Payment/Model/Method/InstanceFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..c273c71e78330df854eabfb4b564675af028cb23 --- /dev/null +++ b/app/code/Magento/Payment/Model/Method/InstanceFactory.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Payment\Model\Method; + +use Magento\Payment\Api\Data\PaymentMethodInterface; + +/** + * Payment method instance factory. + */ +class InstanceFactory +{ + /** + * @var \Magento\Payment\Helper\Data + */ + private $helper; + + /** + * @param \Magento\Payment\Helper\Data $helper + */ + public function __construct( + \Magento\Payment\Helper\Data $helper + ) { + $this->helper = $helper; + } + + /** + * Create payment method instance. + * + * @param PaymentMethodInterface $paymentMethod + * @return \Magento\Payment\Model\MethodInterface + */ + public function create(PaymentMethodInterface $paymentMethod) + { + $methodInstance = $this->helper->getMethodInstance($paymentMethod->getCode()); + $methodInstance->setStore($paymentMethod->getStoreId()); + + return $methodInstance; + } +} diff --git a/app/code/Magento/Payment/Model/MethodList.php b/app/code/Magento/Payment/Model/MethodList.php index d547fe42f332ca4c49f0ff51ac16c0b59e538556..ff1f786c77f6175ab8d78feac5ee7d270a64fc45 100644 --- a/app/code/Magento/Payment/Model/MethodList.php +++ b/app/code/Magento/Payment/Model/MethodList.php @@ -6,12 +6,14 @@ namespace Magento\Payment\Model; +use Magento\Framework\App\ObjectManager; use Magento\Payment\Model\Method\AbstractMethod; class MethodList { /** * @var \Magento\Payment\Helper\Data + * @deprecated */ protected $paymentHelper; @@ -20,6 +22,16 @@ class MethodList */ protected $methodSpecificationFactory; + /** + * @var \Magento\Payment\Api\PaymentMethodListInterface + */ + private $paymentMethodList; + + /** + * @var \Magento\Payment\Model\Method\InstanceFactory + */ + private $paymentMethodInstanceFactory; + /** * @param \Magento\Payment\Helper\Data $paymentHelper * @param Checks\SpecificationFactory $specificationFactory @@ -40,14 +52,16 @@ class MethodList public function getAvailableMethods(\Magento\Quote\Api\Data\CartInterface $quote = null) { $store = $quote ? $quote->getStoreId() : null; - $methods = []; - foreach ($this->paymentHelper->getStoreMethods($store, $quote) as $method) { - if ($this->_canUseMethod($method, $quote)) { - $method->setInfoInstance($quote->getPayment()); - $methods[] = $method; + $availableMethods = []; + + foreach ($this->getPaymentMethodList()->getActiveList($store) as $method) { + $methodInstance = $this->getPaymentMethodInstanceFactory()->create($method); + if ($methodInstance->isAvailable($quote) && $this->_canUseMethod($methodInstance, $quote)) { + $methodInstance->setInfoInstance($quote->getPayment()); + $availableMethods[] = $methodInstance; } } - return $methods; + return $availableMethods; } /** @@ -71,4 +85,36 @@ class MethodList $quote ); } + + /** + * Get payment method list. + * + * @return \Magento\Payment\Api\PaymentMethodListInterface + * @deprecated + */ + private function getPaymentMethodList() + { + if ($this->paymentMethodList === null) { + $this->paymentMethodList = ObjectManager::getInstance()->get( + \Magento\Payment\Api\PaymentMethodListInterface::class + ); + } + return $this->paymentMethodList; + } + + /** + * Get payment method instance factory. + * + * @return \Magento\Payment\Model\Method\InstanceFactory + * @deprecated + */ + private function getPaymentMethodInstanceFactory() + { + if ($this->paymentMethodInstanceFactory === null) { + $this->paymentMethodInstanceFactory = ObjectManager::getInstance()->get( + \Magento\Payment\Model\Method\InstanceFactory::class + ); + } + return $this->paymentMethodInstanceFactory; + } } diff --git a/app/code/Magento/Payment/Model/PaymentMethod.php b/app/code/Magento/Payment/Model/PaymentMethod.php new file mode 100644 index 0000000000000000000000000000000000000000..581c4703f777e1a853dfd15f9afb8c8c7bcc67a6 --- /dev/null +++ b/app/code/Magento/Payment/Model/PaymentMethod.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Payment\Model; + +/** + * Payment method class. + */ +class PaymentMethod implements \Magento\Payment\Api\Data\PaymentMethodInterface +{ + /** + * @var string + */ + private $code; + + /** + * @var string + */ + private $title; + + /** + * @var int + */ + private $storeId; + + /** + * @var bool + */ + private $isActive; + + /** + * @param string $code + * @param string $title + * @param int $storeId + * @param bool $isActive + */ + public function __construct($code, $title, $storeId, $isActive) + { + $this->code = $code; + $this->title = $title; + $this->storeId = $storeId; + $this->isActive = $isActive; + } + + /** + * {@inheritdoc} + */ + public function getCode() + { + return $this->code; + } + + /** + * {@inheritdoc} + */ + public function getTitle() + { + return $this->title; + } + + /** + * {@inheritdoc} + */ + public function getStoreId() + { + return $this->storeId; + } + + /** + * {@inheritdoc} + */ + public function getIsActive() + { + return $this->isActive; + } +} diff --git a/app/code/Magento/Payment/Model/PaymentMethodList.php b/app/code/Magento/Payment/Model/PaymentMethodList.php new file mode 100644 index 0000000000000000000000000000000000000000..5968d201ca6869ab9d38327dc422b7f0b935956d --- /dev/null +++ b/app/code/Magento/Payment/Model/PaymentMethodList.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Payment\Model; + +use Magento\Payment\Api\Data\PaymentMethodInterface; + +/** + * Payment method list class. + */ +class PaymentMethodList implements \Magento\Payment\Api\PaymentMethodListInterface +{ + /** + * @var \Magento\Payment\Api\Data\PaymentMethodInterfaceFactory + */ + private $methodFactory; + + /** + * @var \Magento\Payment\Helper\Data + */ + private $helper; + + /** + * @param \Magento\Payment\Api\Data\PaymentMethodInterfaceFactory $methodFactory + * @param \Magento\Payment\Helper\Data $helper + */ + public function __construct( + \Magento\Payment\Api\Data\PaymentMethodInterfaceFactory $methodFactory, + \Magento\Payment\Helper\Data $helper + ) { + $this->methodFactory = $methodFactory; + $this->helper = $helper; + } + + /** + * {@inheritdoc} + */ + public function getList($storeId) + { + $methodsCodes = array_keys($this->helper->getPaymentMethods()); + + $methodsInstances = array_map( + function ($code) { + return $this->helper->getMethodInstance($code); + }, + $methodsCodes + ); + + $methodsInstances = array_filter($methodsInstances, function (MethodInterface $method) { + return !($method instanceof \Magento\Payment\Model\Method\Substitution); + }); + + @uasort( + $methodsInstances, + function (MethodInterface $a, MethodInterface $b) use ($storeId) { + return (int)$a->getConfigData('sort_order', $storeId) - (int)$b->getConfigData('sort_order', $storeId); + } + ); + + $methodList = array_map( + function (MethodInterface $methodInstance) use ($storeId) { + + return $this->methodFactory->create([ + 'code' => (string)$methodInstance->getCode(), + 'title' => (string)$methodInstance->getTitle(), + 'storeId' => (int)$storeId, + 'isActive' => (bool)$methodInstance->isActive($storeId) + ]); + }, + $methodsInstances + ); + + return array_values($methodList); + } + + /** + * {@inheritdoc} + */ + public function getActiveList($storeId) + { + $methodList = array_filter( + $this->getList($storeId), + function (PaymentMethodInterface $method) { + return $method->getIsActive(); + } + ); + + return array_values($methodList); + } +} diff --git a/app/code/Magento/Payment/Test/Unit/Gateway/Http/Client/SoapTest.php b/app/code/Magento/Payment/Test/Unit/Gateway/Http/Client/SoapTest.php index b8e1b0dcdf973dd9311c67dceccccbe3ad2f203b..2a3ae9c3063730aa8743b33c44aa831b6664f19c 100644 --- a/app/code/Magento/Payment/Test/Unit/Gateway/Http/Client/SoapTest.php +++ b/app/code/Magento/Payment/Test/Unit/Gateway/Http/Client/SoapTest.php @@ -48,6 +48,7 @@ class SoapTest extends \PHPUnit_Framework_TestCase \Magento\Payment\Gateway\Http\ConverterInterface::class )->getMockForAbstractClass(); $this->client = $this->getMockBuilder(\SoapClient::class) + ->setMethods(['__setSoapHeaders', '__soapCall', '__getLastRequest']) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Payment/Test/Unit/Model/MethodListTest.php b/app/code/Magento/Payment/Test/Unit/Model/MethodListTest.php index 915ccddc05c89ac7b3bbbf836aaa80dc7f0f1c4e..872782407c12994e43aa4a026f4a340450b26b88 100644 --- a/app/code/Magento/Payment/Test/Unit/Model/MethodListTest.php +++ b/app/code/Magento/Payment/Test/Unit/Model/MethodListTest.php @@ -24,9 +24,14 @@ class MethodListTest extends \PHPUnit_Framework_TestCase protected $objectManager; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Payment\Api\PaymentMethodListInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentMethodList; + + /** + * @var \Magento\Payment\Model\Method\InstanceFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $paymentHelperMock; + private $paymentMethodInstanceFactory; /** * @var \PHPUnit_Framework_MockObject_MockObject @@ -36,17 +41,35 @@ class MethodListTest extends \PHPUnit_Framework_TestCase protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->paymentHelperMock = $this->getMock(\Magento\Payment\Helper\Data::class, [], [], '', false); - $this->specificationFactoryMock = $this->getMock( + + $this->paymentMethodList = $this->getMockBuilder(\Magento\Payment\Api\PaymentMethodListInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->paymentMethodInstanceFactory = $this->getMockBuilder( + \Magento\Payment\Model\Method\InstanceFactory::class + )->disableOriginalConstructor()->getMock(); + + $this->specificationFactoryMock = $this->getMock( \Magento\Payment\Model\Checks\SpecificationFactory::class, [], [], '', false ); - $this->methodList = $this->objectManager->getObject( + $this->methodList = $this->objectManager->getObject( \Magento\Payment\Model\MethodList::class, [ - 'paymentHelper' => $this->paymentHelperMock, 'specificationFactory' => $this->specificationFactoryMock ] ); + + $this->objectManager->setBackwardCompatibleProperty( + $this->methodList, + 'paymentMethodList', + $this->paymentMethodList + ); + $this->objectManager->setBackwardCompatibleProperty( + $this->methodList, + 'paymentMethodInstanceFactory', + $this->paymentMethodInstanceFactory + ); } public function testGetAvailableMethods() @@ -58,12 +81,15 @@ class MethodListTest extends \PHPUnit_Framework_TestCase ->method('getPayment') ->will($this->returnValue($this->getMock(\Magento\Quote\Model\Quote\Payment::class, [], [], '', false))); - $methodMock = $this->getMock(\Magento\Payment\Model\Method\AbstractMethod::class, [], [], '', false); + $methodInstanceMock = $this->getMock(\Magento\Payment\Model\Method\AbstractMethod::class, [], [], '', false); + $methodInstanceMock->expects($this->once()) + ->method('isAvailable') + ->willReturn(true); $compositeMock = $this->getMock(\Magento\Payment\Model\Checks\Composite::class, [], [], '', false); $compositeMock->expects($this->atLeastOnce()) ->method('isApplicable') - ->with($methodMock, $quoteMock) + ->with($methodInstanceMock, $quoteMock) ->will($this->returnValue(true)); $this->specificationFactoryMock->expects($this->atLeastOnce()) @@ -76,18 +102,19 @@ class MethodListTest extends \PHPUnit_Framework_TestCase ]) ->will($this->returnValue($compositeMock)); - $storeMethods = [$methodMock]; - - $this->paymentHelperMock->expects($this->once()) - ->method('getStoreMethods') - ->with($storeId, $quoteMock) - ->will($this->returnValue($storeMethods)); + $methodMock = $this->getMockForAbstractClass(\Magento\Payment\Api\Data\PaymentMethodInterface::class); + $this->paymentMethodList->expects($this->once()) + ->method('getActiveList') + ->willReturn([$methodMock]); + $this->paymentMethodInstanceFactory->expects($this->once()) + ->method('create') + ->willReturn($methodInstanceMock); - $methodMock->expects($this->atLeastOnce()) + $methodInstanceMock->expects($this->atLeastOnce()) ->method('setInfoInstance') ->with($this->getMock(\Magento\Quote\Model\Quote\Payment::class, [], [], '', false)) ->will($this->returnSelf()); - $this->assertEquals([$methodMock], $this->methodList->getAvailableMethods($quoteMock)); + $this->assertEquals([$methodInstanceMock], $this->methodList->getAvailableMethods($quoteMock)); } } diff --git a/app/code/Magento/Payment/Test/Unit/Model/PaymentMethodListTest.php b/app/code/Magento/Payment/Test/Unit/Model/PaymentMethodListTest.php new file mode 100644 index 0000000000000000000000000000000000000000..c41e5ac2a1aac326970c6a7a2d39eab30b8a8bcc --- /dev/null +++ b/app/code/Magento/Payment/Test/Unit/Model/PaymentMethodListTest.php @@ -0,0 +1,215 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Payment\Test\Unit\Model; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * Class PaymentMethodListTest. + */ +class PaymentMethodListTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var \Magento\Payment\Model\PaymentMethodList|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentMethodList; + + /** + * @var \Magento\Payment\Api\Data\PaymentMethodInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $methodFactoryMock; + + /** + * @var \Magento\Payment\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + */ + private $helperMock; + + /** + * Setup. + * + * @return void + */ + public function setUp() + { + $this->methodFactoryMock = $this->getMockBuilder(\Magento\Payment\Api\Data\PaymentMethodInterfaceFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $this->helperMock = $this->getMockBuilder(\Magento\Payment\Helper\Data::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->paymentMethodList = $this->objectManagerHelper->getObject( + \Magento\Payment\Model\PaymentMethodList::class, + [ + 'methodFactory' => $this->methodFactoryMock, + 'helper' => $this->helperMock + ] + ); + } + + /** + * Setup getList method. + * + * @param array $paymentMethodConfig + * @param array $methodInstancesMap + * @return void + */ + private function setUpGetList($paymentMethodConfig, $methodInstancesMap) + { + $this->helperMock->expects($this->once()) + ->method('getPaymentMethods') + ->willReturn($paymentMethodConfig); + $this->helperMock->expects($this->any()) + ->method('getMethodInstance') + ->willReturnMap($methodInstancesMap); + + $this->methodFactoryMock->expects($this->any()) + ->method('create') + ->willReturnCallback(function ($data) { + $paymentMethod = $this->getMockBuilder(\Magento\Payment\Api\Data\PaymentMethodInterface::class) + ->getMockForAbstractClass(); + $paymentMethod->expects($this->any()) + ->method('getCode') + ->willReturn($data['code']); + $paymentMethod->expects($this->any()) + ->method('getIsActive') + ->willReturn($data['isActive']); + + return $paymentMethod; + }); + } + + /** + * Test getList. + * + * @param int $storeId + * @param array $paymentMethodConfig + * @param array $methodInstancesMap + * @param array $expected + * @return void + * + * @dataProvider getListDataProvider + */ + public function testGetList($storeId, $paymentMethodConfig, $methodInstancesMap, $expected) + { + $this->setUpGetList($paymentMethodConfig, $methodInstancesMap); + + $codes = array_map( + function ($method) { + return $method->getCode(); + }, + $this->paymentMethodList->getList($storeId) + ); + + $this->assertEquals($expected, $codes); + } + + /** + * Data provider for getList. + * + * @return array + */ + public function getListDataProvider() + { + return [ + [ + 1, + ['method_code_1' => [], 'method_code_2' => []], + [ + ['method_code_1', $this->mockPaymentMethodInstance(1, 10, 'method_code_1', 'title', true)], + ['method_code_2', $this->mockPaymentMethodInstance(1, 5, 'method_code_2', 'title', true)] + ], + ['method_code_2', 'method_code_1'] + ] + ]; + } + + /** + * Test getActiveList. + * + * @param int $storeId + * @param array $paymentMethodConfig + * @param array $methodInstancesMap + * @param array $expected + * @return void + * + * @dataProvider getActiveListDataProvider + */ + public function testGetActiveList($storeId, $paymentMethodConfig, $methodInstancesMap, $expected) + { + $this->setUpGetList($paymentMethodConfig, $methodInstancesMap); + + $codes = array_map( + function ($method) { + return $method->getCode(); + }, + $this->paymentMethodList->getActiveList($storeId) + ); + + $this->assertEquals($expected, $codes); + } + + /** + * Data provider for getActiveList. + * + * @return array + */ + public function getActiveListDataProvider() + { + return [ + [ + 1, + ['method_code_1' => [], 'method_code_2' => []], + [ + ['method_code_1', $this->mockPaymentMethodInstance(1, 10, 'method_code_1', 'title', false)], + ['method_code_2', $this->mockPaymentMethodInstance(1, 5, 'method_code_2', 'title', true)] + ], + ['method_code_2'] + ] + ]; + } + + /** + * Mock payment method instance. + * + * @param int $storeId + * @param int $sortOrder + * @param string $code + * @param string $title + * @param bool $isActive + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function mockPaymentMethodInstance($storeId, $sortOrder, $code, $title, $isActive) + { + $paymentMethodInstance = $this->getMockBuilder(\Magento\Payment\Model\Method\AbstractMethod::class) + ->setMethods(['getCode', 'getTitle', 'isActive', 'getConfigData']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $paymentMethodInstance->expects($this->any()) + ->method('getConfigData') + ->willReturnMap([ + ['sort_order', $storeId, $sortOrder] + ]); + $paymentMethodInstance->expects($this->any()) + ->method('getCode') + ->willReturn($code); + $paymentMethodInstance->expects($this->any()) + ->method('getTitle') + ->willReturn($title); + $paymentMethodInstance->expects($this->any()) + ->method('isActive') + ->willReturn($isActive); + + return $paymentMethodInstance; + } +} diff --git a/app/code/Magento/Payment/etc/di.xml b/app/code/Magento/Payment/etc/di.xml index 73f8e2a76334be8c0bde6f1a2c69a063818ca928..c12c2d2cfd51c2be7688aacef7b023e618f2f77a 100644 --- a/app/code/Magento/Payment/etc/di.xml +++ b/app/code/Magento/Payment/etc/di.xml @@ -6,6 +6,8 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\Payment\Api\Data\PaymentMethodInterface" type="Magento\Payment\Model\PaymentMethod"/> + <preference for="Magento\Payment\Api\PaymentMethodListInterface" type="Magento\Payment\Model\PaymentMethodList"/> <preference for="Magento\Payment\Gateway\Validator\ResultInterface" type="Magento\Payment\Gateway\Validator\Result"/> <preference for="Magento\Payment\Gateway\ConfigFactoryInterface" type="Magento\Payment\Gateway\Config\ConfigFactory" /> <preference for="Magento\Payment\Gateway\Command\CommandManagerPoolInterface" type="Magento\Payment\Gateway\Command\CommandManagerPool" /> diff --git a/app/code/Magento/Payment/view/frontend/web/js/view/payment/iframe.js b/app/code/Magento/Payment/view/frontend/web/js/view/payment/iframe.js index c8a6fef58d31ea24dcb930b5e63c6912f8738eff..7d01c195791e482618ae2073a82564cb14aaf339 100644 --- a/app/code/Magento/Payment/view/frontend/web/js/view/payment/iframe.js +++ b/app/code/Magento/Payment/view/frontend/web/js/view/payment/iframe.js @@ -154,6 +154,7 @@ define( */ clearTimeout: function () { clearTimeout(this.timeoutId); + this.fail(); return this; }, diff --git a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Payment.php b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Payment.php index 4abe12cbc3715d5192a6b1a4f0a2ae44685a368d..725ebb2282418e9e6747c3fc28c72ff600f31e07 100644 --- a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Payment.php +++ b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Payment.php @@ -78,18 +78,10 @@ class Payment extends \Magento\Config\Block\System\Config\Form\Fieldset */ protected function _getHeaderTitleHtml($element) { - $html = '<div class="config-heading" ><div class="heading"><strong>' . $element->getLegend(); + $html = '<div class="config-heading" >'; $groupConfig = $element->getGroup(); - $html .= '</strong>'; - - if ($element->getComment()) { - $html .= '<span class="heading-intro">' . $element->getComment() . '</span>'; - } - $html .= '<div class="config-alt"></div>'; - $html .= '</div>'; - $disabledAttributeString = $this->_isPaymentEnabled($element) ? '' : ' disabled="disabled"'; $disabledClassString = $this->_isPaymentEnabled($element) ? '' : ' disabled'; $htmlId = $element->getHtmlId(); @@ -122,6 +114,13 @@ class Payment extends \Magento\Config\Block\System\Config\Form\Fieldset ) . '</a>'; } + $html .= '</div>'; + $html .= '<div class="heading"><strong>' . $element->getLegend() . '</strong>'; + + if ($element->getComment()) { + $html .= '<span class="heading-intro">' . $element->getComment() . '</span>'; + } + $html .= '<div class="config-alt"></div>'; $html .= '</div></div>'; return $html; diff --git a/app/code/Magento/Paypal/Helper/Data.php b/app/code/Magento/Paypal/Helper/Data.php index 0ecde3bdc5697488e33e3c027a6d0c9a2439d03d..ea69d2ffcf0794f09497d66444d35600a3d8e077 100644 --- a/app/code/Magento/Paypal/Helper/Data.php +++ b/app/code/Magento/Paypal/Helper/Data.php @@ -5,6 +5,8 @@ */ namespace Magento\Paypal\Helper; +use Magento\Framework\App\ObjectManager; +use Magento\Payment\Model\Method\AbstractMethod; use Magento\Paypal\Model\Billing\Agreement\MethodInterface; /** @@ -42,6 +44,16 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper */ private $configFactory; + /** + * @var \Magento\Payment\Api\PaymentMethodListInterface + */ + private $paymentMethodList; + + /** + * @var \Magento\Payment\Model\Method\InstanceFactory + */ + private $paymentMethodInstanceFactory; + /** * @param \Magento\Framework\App\Helper\Context $context * @param \Magento\Payment\Helper\Data $paymentData @@ -92,12 +104,20 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper */ public function getBillingAgreementMethods($store = null, $quote = null) { - $result = []; - foreach ($this->_paymentData->getStoreMethods($store, $quote) as $method) { - if ($method instanceof MethodInterface) { - $result[] = $method; + $activeMethods = array_map( + function (\Magento\Payment\Api\Data\PaymentMethodInterface $method) { + return $this->getPaymentMethodInstanceFactory()->create($method); + }, + $this->getPaymentMethodList()->getActiveList($store) + ); + + $result = array_filter( + $activeMethods, + function (AbstractMethod $method) use ($quote) { + return $method->isAvailable($quote) && $method instanceof MethodInterface; } - } + ); + return $result; } @@ -118,4 +138,36 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper } return $txnId; } + + /** + * Get payment method list. + * + * @return \Magento\Payment\Api\PaymentMethodListInterface + * @deprecated + */ + private function getPaymentMethodList() + { + if ($this->paymentMethodList === null) { + $this->paymentMethodList = ObjectManager::getInstance()->get( + \Magento\Payment\Api\PaymentMethodListInterface::class + ); + } + return $this->paymentMethodList; + } + + /** + * Get payment method instance factory. + * + * @return \Magento\Payment\Model\Method\InstanceFactory + * @deprecated + */ + private function getPaymentMethodInstanceFactory() + { + if ($this->paymentMethodInstanceFactory === null) { + $this->paymentMethodInstanceFactory = ObjectManager::getInstance()->get( + \Magento\Payment\Model\Method\InstanceFactory::class + ); + } + return $this->paymentMethodInstanceFactory; + } } diff --git a/app/code/Magento/Paypal/Model/Config.php b/app/code/Magento/Paypal/Model/Config.php index e02615d24ad3d2fbd4e90a768ce9594531acc602..0af8b18355ec32b045c4e609137cb0eed7944373 100644 --- a/app/code/Magento/Paypal/Model/Config.php +++ b/app/code/Magento/Paypal/Model/Config.php @@ -866,7 +866,9 @@ class Config extends AbstractConfig */ public function getExpressCheckoutStartUrl($token) { - return $this->getPaypalUrl(['cmd' => '_express-checkout', 'token' => $token]); + return sprintf('https://www.%spaypal.com/checkoutnow/2%s', + $this->getValue('sandboxFlag') ? 'sandbox.' : '', + '?token=' . urlencode($token)); } /** diff --git a/app/code/Magento/Paypal/Model/Express.php b/app/code/Magento/Paypal/Model/Express.php index d377e8ed626e15a65a3772041bb837328a4942f3..db1ecdf6b33799673cd22926f5d9dd1b7999f965 100644 --- a/app/code/Magento/Paypal/Model/Express.php +++ b/app/code/Magento/Paypal/Model/Express.php @@ -672,19 +672,13 @@ class Express extends \Magento\Payment\Model\Method\AbstractMethod $additionalData = $data->getData(PaymentInterface::KEY_ADDITIONAL_DATA); - if ( - !is_array($additionalData) - || !isset($additionalData[ExpressCheckout::PAYMENT_INFO_TRANSPORT_BILLING_AGREEMENT]) - ) { + if (!is_array($additionalData)) { return $this; } - $this->getInfoInstance() - ->setAdditionalInformation( - ExpressCheckout::PAYMENT_INFO_TRANSPORT_BILLING_AGREEMENT, - $additionalData[ExpressCheckout::PAYMENT_INFO_TRANSPORT_BILLING_AGREEMENT] - ); - + foreach ($additionalData as $key => $value) { + $this->getInfoInstance()->setAdditionalInformation($key, $value); + } return $this; } diff --git a/app/code/Magento/Paypal/Model/Payflow/Transparent.php b/app/code/Magento/Paypal/Model/Payflow/Transparent.php index f0383dffc9b2443f96bf903b1b423306ff09b4ea..b5803c5ace3925b75e81df43992f714a74ed9905 100644 --- a/app/code/Magento/Paypal/Model/Payflow/Transparent.php +++ b/app/code/Magento/Paypal/Model/Payflow/Transparent.php @@ -166,6 +166,9 @@ class Transparent extends Payflowpro implements TransparentInterface $request->setData('trxtype', self::TRXTYPE_AUTH_ONLY); $request->setData('origid', $token); $request->setData('amt', $this->formatPrice($amount)); + $request->setData('currency', $order->getBaseCurrencyCode()); + $request->setData('taxamt', $this->formatPrice($order->getBaseTaxAmount())); + $request->setData('freightamt', $this->formatPrice($order->getBaseShippingAmount())); $response = $this->postRequest($request, $this->getConfig()); $this->processErrors($response); diff --git a/app/code/Magento/Paypal/Test/Unit/Helper/DataTest.php b/app/code/Magento/Paypal/Test/Unit/Helper/DataTest.php index fa0620a36a681eae6a48413044c6784876aaa889..239c5d6cda746d62fdc438791bb29bfc43f61c19 100644 --- a/app/code/Magento/Paypal/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Helper/DataTest.php @@ -8,9 +8,14 @@ namespace Magento\Paypal\Test\Unit\Helper; class DataTest extends \PHPUnit_Framework_TestCase { /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Payment\Api\PaymentMethodListInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $_paymentDataMock; + private $paymentMethodList; + + /** + * @var \Magento\Payment\Model\Method\InstanceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentMethodInstanceFactory; /** * @var \Magento\Paypal\Model\Config | \PHPUnit_Framework_MockObject_MockObject @@ -24,11 +29,13 @@ class DataTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->_paymentDataMock = $this->getMockBuilder( - \Magento\Payment\Helper\Data::class - )->disableOriginalConstructor()->setMethods( - ['getStoreMethods', 'getPaymentMethods'] - )->getMock(); + $this->paymentMethodList = $this->getMockBuilder(\Magento\Payment\Api\PaymentMethodListInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->paymentMethodInstanceFactory = $this->getMockBuilder( + \Magento\Payment\Model\Method\InstanceFactory::class + )->disableOriginalConstructor()->getMock(); $this->configMock = $this->getMock( \Magento\Paypal\Model\Config::class, @@ -48,32 +55,41 @@ class DataTest extends \PHPUnit_Framework_TestCase $this->_helper = $objectManager->getObject( \Magento\Paypal\Helper\Data::class, [ - 'paymentData' => $this->_paymentDataMock, 'methodCodes' => ['expressCheckout' => 'paypal_express', 'hostedPro' => 'hosted_pro'], 'configFactory' => $configMockFactory ] ); + + $objectManager->setBackwardCompatibleProperty( + $this->_helper, + 'paymentMethodList', + $this->paymentMethodList + ); + $objectManager->setBackwardCompatibleProperty( + $this->_helper, + 'paymentMethodInstanceFactory', + $this->paymentMethodInstanceFactory + ); } /** * @dataProvider getBillingAgreementMethodsDataProvider * @param $store * @param $quote - * @param $paymentMethods + * @param $paymentMethodsMap * @param $expectedResult */ - public function testGetBillingAgreementMethods($store, $quote, $paymentMethods, $expectedResult) + public function testGetBillingAgreementMethods($store, $quote, $paymentMethodsMap, $expectedResult) { - $this->_paymentDataMock->expects( - $this->any() - )->method( - 'getStoreMethods' - )->with( - $store, - $quote - )->will( - $this->returnValue($paymentMethods) - ); + $this->paymentMethodList->expects(static::once()) + ->method('getActiveList') + ->with($store) + ->willReturn(array_column($paymentMethodsMap, 0)); + + $this->paymentMethodInstanceFactory->expects(static::any()) + ->method('create') + ->willReturnMap($paymentMethodsMap); + $this->assertEquals($expectedResult, $this->_helper->getBillingAgreementMethods($store, $quote)); } @@ -84,16 +100,40 @@ class DataTest extends \PHPUnit_Framework_TestCase { $quoteMock = $this->getMockBuilder( \Magento\Quote\Model\Quote::class - )->disableOriginalConstructor()->setMethods( - null - ); - $methodInterfaceMock = $this->getMockBuilder( - \Magento\Paypal\Model\Billing\Agreement\MethodInterface::class + )->disableOriginalConstructor()->getMock(); + + $methodMock = $this->getMockBuilder( + \Magento\Payment\Api\Data\PaymentMethodInterface::class )->getMock(); + $agreementMethodInstanceMock = $this->getMockBuilder( + \Magento\Paypal\Model\Method\Agreement::class + )->disableOriginalConstructor()->getMock(); + $agreementMethodInstanceMock->expects($this->any()) + ->method('isAvailable') + ->willReturn(true); + + $methodInstanceMock = $this->getMockBuilder( + \Magento\Payment\Model\Method\Cc::class + )->disableOriginalConstructor()->getMock(); + return [ - ['1', $quoteMock, [$methodInterfaceMock], [$methodInterfaceMock]], - ['1', $quoteMock, [new \StdClass()], []] + [ + '1', + $quoteMock, + [ + [$methodMock, $agreementMethodInstanceMock] + ], + [$agreementMethodInstanceMock] + ], + [ + '1', + $quoteMock, + [ + [$methodMock, $methodInstanceMock] + ], + [] + ] ]; } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/ExpressTest.php b/app/code/Magento/Paypal/Test/Unit/Model/ExpressTest.php index 33065cc22713bcb654f8304505717288a9f21e0b..9816c37fa01267fa1e1952af91c76bab1fd4cf5c 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/ExpressTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/ExpressTest.php @@ -182,7 +182,9 @@ class ExpressTest extends \PHPUnit_Framework_TestCase $data = new DataObject( [ PaymentInterface::KEY_ADDITIONAL_DATA => [ - Express\Checkout::PAYMENT_INFO_TRANSPORT_BILLING_AGREEMENT => $transportValue + Express\Checkout::PAYMENT_INFO_TRANSPORT_BILLING_AGREEMENT => $transportValue, + Express\Checkout::PAYMENT_INFO_TRANSPORT_PAYER_ID => $transportValue, + Express\Checkout::PAYMENT_INFO_TRANSPORT_TOKEN => $transportValue ] ] ); @@ -202,11 +204,12 @@ class ExpressTest extends \PHPUnit_Framework_TestCase $this->parentAssignDataExpectation($data); - $paymentInfo->expects(static::once()) + $paymentInfo->expects(static::exactly(3)) ->method('setAdditionalInformation') - ->with( - Express\Checkout::PAYMENT_INFO_TRANSPORT_BILLING_AGREEMENT, - $transportValue + ->withConsecutive( + [Express\Checkout::PAYMENT_INFO_TRANSPORT_BILLING_AGREEMENT, $transportValue], + [Express\Checkout::PAYMENT_INFO_TRANSPORT_PAYER_ID, $transportValue], + [Express\Checkout::PAYMENT_INFO_TRANSPORT_TOKEN, $transportValue] ); $this->_model->assignData($data); diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/TransparentTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/TransparentTest.php index a107918d16513b2fdce133e43aef00c5109798c8..2d757147cec6a89dc1b6b6b28f283fd29b98f640 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/TransparentTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/TransparentTest.php @@ -122,7 +122,7 @@ class TransparentTest extends \PHPUnit_Framework_TestCase $this->orderMock = $this->getMockBuilder(\Magento\Sales\Model\Order::class) ->setMethods([ 'getCustomerId', 'getBillingAddress', 'getShippingAddress', 'getCustomerEmail', - 'getId', 'getIncrementId' + 'getId', 'getIncrementId', 'getBaseCurrencyCode' ]) ->disableOriginalConstructor() ->getMock(); @@ -164,6 +164,9 @@ class TransparentTest extends \PHPUnit_Framework_TestCase $this->paymentMock->expects($this->once()) ->method('getOrder') ->willReturn($this->orderMock); + $this->orderMock->expects($this->once()) + ->method('getBaseCurrencyCode') + ->willReturn('USD'); $this->orderMock->expects($this->once()) ->method('getBillingAddress') ->willReturn($this->addressBillingMock); diff --git a/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro.xml b/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro.xml index b567ac87c4fe253b7b0e6423fb7e25f575521462..98f340f0a271135b241bcdc9a90f2e195f3d76b5 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro.xml @@ -84,7 +84,7 @@ </requires> </field> <field id="payflowpro_cc_vault_active" translate="label" type="select" sortOrder="22" showInDefault="1" showInWebsite="1" showInStore="0"> - <label>Vault enabled</label> + <label>Vault Enabled</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <config_path>payment/payflowpro_cc_vault/active</config_path> <attribute type="shared">1</attribute> diff --git a/app/code/Magento/Paypal/view/adminhtml/web/styles.css b/app/code/Magento/Paypal/view/adminhtml/web/styles.css index 9e7cfb2afc18e570c18fdc3fe96920606cbe255a..9119a2e317fb75062e74a9e6543732645fab76d5 100644 --- a/app/code/Magento/Paypal/view/adminhtml/web/styles.css +++ b/app/code/Magento/Paypal/view/adminhtml/web/styles.css @@ -14,12 +14,11 @@ .payflow-settings-notice { border:1px solid #d1d0ce;padding:0 10px;margin: 0;} .payflow-settings-notice .important-label {color:#f00;} .payflow-settings-notice ul.options-list {list-style:disc;padding:0 2em;} -.paypal-express-section .heading {display: inline-block; background: url("images/pp-logo-200px.png") no-repeat 0 50% / 18rem auto; padding-left: 20rem;} -.paypal-express-section .button-container {display: inline-block; float: right;} -.paypal-express-section .config-alt {background: url("images/pp-alt.png") no-repeat; height: 26px; margin: .5rem 0 0; width: 158px;} +.paypal-express-section .heading {background: url("images/pp-logo-200px.png") no-repeat 0 50% / 18rem auto; padding-left: 20rem;} +.paypal-express-section .button-container {float: right;} +.paypal-express-section .config-alt {background: url("images/pp-alt.png") no-repeat; height: 26px; margin: 0.5rem 0 0; width: 158px;} .paypal-express-section .link-more {margin-left: 5px;} -.paypal-other-section .heading {display: inline-block;} -.paypal-other-section .button-container {display: inline-block; float: right; margin: 1rem 0 0 !important;} +.paypal-other-section .button-container {float: right; margin: 1rem 0 0 !important;} .paypal-other-section > .entry-edit-head > a::before {left: auto !important; right: 1.3rem !important;} .paypal-all-in-one-section > .entry-edit-head {background: url("images/AM_mc_vs_dc_ae.jpg") no-repeat scroll 0 50% / 18rem auto; padding-left: 18rem;} .paypal-gateways-section > .entry-edit-head {background: url("images/pp-payflow-mark.png") no-repeat scroll 0 50% / 18rem auto; padding-left: 18rem;} diff --git a/app/code/Magento/Sales/Api/Data/CommentInterface.php b/app/code/Magento/Sales/Api/Data/CommentInterface.php index fcab786319340535c1c6309c884a678a91341d90..6e447e72ab1c21330ecd56ed81c1fd7bd5114865 100644 --- a/app/code/Magento/Sales/Api/Data/CommentInterface.php +++ b/app/code/Magento/Sales/Api/Data/CommentInterface.php @@ -23,14 +23,14 @@ interface CommentInterface const COMMENT = 'comment'; /** - * Gets the comment for the invoice. + * Gets the comment text. * * @return string Comment. */ public function getComment(); /** - * Sets the comment for the invoice. + * Sets the comment text. * * @param string $comment * @return $this @@ -38,14 +38,14 @@ interface CommentInterface public function setComment($comment); /** - * Gets the is-visible-on-storefront flag value for the invoice. + * Gets the is-visible-on-storefront flag value for the comment. * * @return int Is-visible-on-storefront flag value. */ public function getIsVisibleOnFront(); /** - * Sets the is-visible-on-storefront flag value for the invoice. + * Sets the is-visible-on-storefront flag value for the comment. * * @param int $isVisibleOnFront * @return $this diff --git a/app/code/Magento/Sales/Api/Data/CreditmemoCommentCreationInterface.php b/app/code/Magento/Sales/Api/Data/CreditmemoCommentCreationInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..283e8600738e731ee498a444309adc36c8123d21 --- /dev/null +++ b/app/code/Magento/Sales/Api/Data/CreditmemoCommentCreationInterface.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Api\Data; + +use Magento\Framework\Api\ExtensibleDataInterface; + +/** + * Interface CreditmemoCommentCreationInterface + * + * @api + */ +interface CreditmemoCommentCreationInterface extends ExtensibleDataInterface, CommentInterface +{ + /** + * Retrieve existing extension attributes object or create a new one. + * + * @return \Magento\Sales\Api\Data\CreditmemoCommentCreationExtensionInterface|null + */ + public function getExtensionAttributes(); + + /** + * Set an extension attributes object. + * + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Magento\Sales\Api\Data\CreditmemoCommentCreationExtensionInterface $extensionAttributes + ); +} diff --git a/app/code/Magento/Sales/Api/Data/CreditmemoCreationArgumentsInterface.php b/app/code/Magento/Sales/Api/Data/CreditmemoCreationArgumentsInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..2735a94a777f095ed5f4b2c97e114fe75085ddc8 --- /dev/null +++ b/app/code/Magento/Sales/Api/Data/CreditmemoCreationArgumentsInterface.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Api\Data; + +/** + * Interface CreditmemoCreationArgumentsInterface + * + * @api + */ +interface CreditmemoCreationArgumentsInterface +{ + /** + * Gets the credit memo shipping amount. + * + * @return float|null Credit memo shipping amount. + */ + public function getShippingAmount(); + + /** + * Gets the credit memo positive adjustment. + * + * @return float|null Credit memo positive adjustment. + */ + public function getAdjustmentPositive(); + + /** + * Gets the credit memo negative adjustment. + * + * @return float|null Credit memo negative adjustment. + */ + public function getAdjustmentNegative(); + + /** + * Sets the credit memo shipping amount. + * + * @param float $amount + * @return $this + */ + public function setShippingAmount($amount); + + /** + * Sets the credit memo positive adjustment. + * + * @param float $amount + * @return $this + */ + public function setAdjustmentPositive($amount); + + /** + * Sets the credit memo negative adjustment. + * + * @param float $amount + * @return $this + */ + public function setAdjustmentNegative($amount); + + /** + * Gets existing extension attributes. + * + * @return \Magento\Sales\Api\Data\CreditmemoCreationArgumentsExtensionInterface|null + */ + public function getExtensionAttributes(); + + /** + * Sets extension attributes. + * + * @param \Magento\Sales\Api\Data\CreditmemoCreationArgumentsExtensionInterface $extensionAttributes + * + * @return $this + */ + public function setExtensionAttributes( + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsExtensionInterface $extensionAttributes + ); +} diff --git a/app/code/Magento/Sales/Api/Data/CreditmemoItemCreationInterface.php b/app/code/Magento/Sales/Api/Data/CreditmemoItemCreationInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..1cd5f3c43be33aa555780af662013a7dda9838d4 --- /dev/null +++ b/app/code/Magento/Sales/Api/Data/CreditmemoItemCreationInterface.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Api\Data; + +use Magento\Framework\Api\ExtensibleDataInterface; + +/** + * Interface CreditmemoItemCreationInterface + * @api + */ +interface CreditmemoItemCreationInterface extends LineItemInterface, ExtensibleDataInterface +{ + /** + * Retrieve existing extension attributes object or create a new one. + * + * @return \Magento\Sales\Api\Data\CreditmemoItemCreationExtensionInterface|null + */ + public function getExtensionAttributes(); + + /** + * Set an extension attributes object. + * + * @param \Magento\Sales\Api\Data\CreditmemoItemCreationExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Magento\Sales\Api\Data\CreditmemoItemCreationExtensionInterface $extensionAttributes + ); +} diff --git a/app/code/Magento/Sales/Api/Exception/CouldNotRefundExceptionInterface.php b/app/code/Magento/Sales/Api/Exception/CouldNotRefundExceptionInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..dee24735af620f08fdedd21f2c95d2d778235959 --- /dev/null +++ b/app/code/Magento/Sales/Api/Exception/CouldNotRefundExceptionInterface.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Api\Exception; + +/** + * @api + */ +interface CouldNotRefundExceptionInterface +{ +} diff --git a/app/code/Magento/Sales/Api/RefundInvoiceInterface.php b/app/code/Magento/Sales/Api/RefundInvoiceInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..dc86ccbb58006b9d8417776ecf731151ea5225da --- /dev/null +++ b/app/code/Magento/Sales/Api/RefundInvoiceInterface.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Api; + +/** + * Interface RefundInvoiceInterface + * + * @api + */ +interface RefundInvoiceInterface +{ + /** + * Create refund for invoice + * + * @param int $invoiceId + * @param \Magento\Sales\Api\Data\CreditmemoItemCreationInterface[] $items + * @param bool|null $isOnline + * @param bool|null $notify + * @param bool|null $appendComment + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment + * @param \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface|null $arguments + * @return int + */ + public function execute( + $invoiceId, + array $items = [], + $isOnline = false, + $notify = false, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ); +} diff --git a/app/code/Magento/Sales/Api/RefundOrderInterface.php b/app/code/Magento/Sales/Api/RefundOrderInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..20a782de421989abfa2c1466c7cd9675a6b20008 --- /dev/null +++ b/app/code/Magento/Sales/Api/RefundOrderInterface.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Api; + +/** + * Interface RefundOrderInterface + * + * @api + */ +interface RefundOrderInterface +{ + /** + * Create offline refund for order + * + * @param int $orderId + * @param \Magento\Sales\Api\Data\CreditmemoItemCreationInterface[] $items + * @param bool|null $notify + * @param bool|null $appendComment + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment + * @param \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface|null $arguments + * @return int + */ + public function execute( + $orderId, + array $items = [], + $notify = false, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ); +} diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Billing/Method/Form.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Billing/Method/Form.php index 9a910e48ec0f2e8344c35795dd544cfe9b1dc506..892faf66140180eb791d4bd8ff25c4b4c48ed930 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Billing/Method/Form.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Billing/Method/Form.php @@ -25,16 +25,18 @@ class Form extends \Magento\Payment\Block\Form\Container * @param \Magento\Payment\Model\Checks\SpecificationFactory $methodSpecificationFactory * @param \Magento\Backend\Model\Session\Quote $sessionQuote * @param array $data + * @param array $additionalChecks */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Magento\Payment\Helper\Data $paymentHelper, \Magento\Payment\Model\Checks\SpecificationFactory $methodSpecificationFactory, \Magento\Backend\Model\Session\Quote $sessionQuote, - array $data = [] + array $data = [], + array $additionalChecks = [] ) { $this->_sessionQuote = $sessionQuote; - parent::__construct($context, $paymentHelper, $methodSpecificationFactory, $data); + parent::__construct($context, $paymentHelper, $methodSpecificationFactory, $data, $additionalChecks); } /** diff --git a/app/code/Magento/Sales/Exception/CouldNotRefundException.php b/app/code/Magento/Sales/Exception/CouldNotRefundException.php new file mode 100644 index 0000000000000000000000000000000000000000..59ef4d18b442eda80a5c72c66282c855ca878bd1 --- /dev/null +++ b/app/code/Magento/Sales/Exception/CouldNotRefundException.php @@ -0,0 +1,16 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Exception; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Sales\Api\Exception\CouldNotRefundExceptionInterface; + +/** + * Class CouldNotRefundException + */ +class CouldNotRefundException extends LocalizedException implements CouldNotRefundExceptionInterface +{ +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo.php b/app/code/Magento/Sales/Model/Order/Creditmemo.php index 02b2826f14f6e4d320e7d5b7ad3a2c9b41ec6710..fa7dd86dbef19568afb2ff84e650518bc629d503 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo.php @@ -532,11 +532,24 @@ class Creditmemo extends AbstractModel implements EntityInterface, CreditmemoInt */ public function isLast() { - foreach ($this->getAllItems() as $item) { + $items = $this->getAllItems(); + foreach ($items as $item) { if (!$item->isLast()) { return false; } } + + if (empty($items)) { + $order = $this->getOrder(); + if ($order) { + foreach ($order->getItems() as $orderItem) { + if ($orderItem->canRefund()) { + return false; + } + } + } + } + return true; } diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/CommentCreation.php b/app/code/Magento/Sales/Model/Order/Creditmemo/CommentCreation.php new file mode 100644 index 0000000000000000000000000000000000000000..b110b2dbd3986bae566914eef3e164b2410939f3 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/CommentCreation.php @@ -0,0 +1,86 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Model\Order\Creditmemo; + +use Magento\Sales\Api\Data\CreditmemoCommentCreationInterface; + +/** + * Class CommentCreation + */ +class CommentCreation implements CreditmemoCommentCreationInterface +{ + /** + * @var \Magento\Sales\Api\Data\CreditmemoCommentCreationExtensionInterface + */ + private $extensionAttributes; + + /** + * @var string + */ + private $comment; + + /** + * @var int + */ + private $isVisibleOnFront; + + /** + * Retrieve existing extension attributes object or create a new one. + * + * @return \Magento\Sales\Api\Data\CreditmemoCommentCreationExtensionInterface|null + */ + public function getExtensionAttributes() + { + return $this->extensionAttributes; + } + + /** + * Set an extension attributes object. + * + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Magento\Sales\Api\Data\CreditmemoCommentCreationExtensionInterface $extensionAttributes + ) { + $this->extensionAttributes = $extensionAttributes; + return $this; + } + + /** + * @inheritdoc + */ + public function getComment() + { + return $this->comment; + } + + /** + * @inheritdoc + */ + public function setComment($comment) + { + $this->comment = $comment; + return $this; + } + + /** + * @inheritdoc + */ + public function getIsVisibleOnFront() + { + return $this->isVisibleOnFront; + } + + /** + * @inheritdoc + */ + public function setIsVisibleOnFront($isVisibleOnFront) + { + $this->isVisibleOnFront = $isVisibleOnFront; + return $this; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/CreationArguments.php b/app/code/Magento/Sales/Model/Order/Creditmemo/CreationArguments.php new file mode 100644 index 0000000000000000000000000000000000000000..fd082bb1dd474b03704b51aec67eba5ea20b23be --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/CreationArguments.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Model\Order\Creditmemo; + +use Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface; + +/** + * Class CreationArguments + */ +class CreationArguments implements CreditmemoCreationArgumentsInterface +{ + /** + * @var float|null + */ + private $shippingAmount; + + /** + * @var float|null + */ + private $adjustmentPositive; + + /** + * @var float|null + */ + private $adjustmentNegative; + + /** + * @var \Magento\Sales\Api\Data\CreditmemoCreationArgumentsExtensionInterface + */ + private $extensionAttributes; + + /** + * @inheritdoc + */ + public function getShippingAmount() + { + return $this->shippingAmount; + } + + /** + * @inheritdoc + */ + public function getAdjustmentPositive() + { + return $this->adjustmentPositive; + } + + /** + * @inheritdoc + */ + public function getAdjustmentNegative() + { + return $this->adjustmentNegative; + } + + /** + * @inheritdoc + */ + public function setShippingAmount($amount) + { + $this->shippingAmount = $amount; + return $this; + } + + /** + * @inheritdoc + */ + public function setAdjustmentPositive($amount) + { + $this->adjustmentPositive = $amount; + return $this; + } + + /** + * @inheritdoc + */ + public function setAdjustmentNegative($amount) + { + $this->adjustmentNegative = $amount; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getExtensionAttributes() + { + return $this->extensionAttributes; + } + + /** + * {@inheritdoc} + */ + public function setExtensionAttributes( + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsExtensionInterface $extensionAttributes + ) { + $this->extensionAttributes = $extensionAttributes; + + return $this; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/CreditmemoValidator.php b/app/code/Magento/Sales/Model/Order/Creditmemo/CreditmemoValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..e49a08e32d83945f1524fb7edaf2f0b6ca5539df --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/CreditmemoValidator.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Model\Order\Creditmemo; + +use Magento\Sales\Api\Data\CreditmemoInterface; + +/** + * Class CreditmemoValidator + */ +class CreditmemoValidator implements CreditmemoValidatorInterface +{ + /** + * @var \Magento\Sales\Model\Validator + */ + private $validator; + + /** + * InvoiceValidatorRunner constructor. + * @param \Magento\Sales\Model\Validator $validator + */ + public function __construct(\Magento\Sales\Model\Validator $validator) + { + $this->validator = $validator; + } + + /** + * @inheritdoc + */ + public function validate(CreditmemoInterface $entity, array $validators) + { + return $this->validator->validate($entity, $validators); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/CreditmemoValidatorInterface.php b/app/code/Magento/Sales/Model/Order/Creditmemo/CreditmemoValidatorInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..3889f3b985ff02603009084fa956fab65e505556 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/CreditmemoValidatorInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Model\Order\Creditmemo; + +use Magento\Sales\Api\Data\CreditmemoInterface; +use Magento\Sales\Exception\DocumentValidationException; +use Magento\Sales\Model\ValidatorInterface; + +/** + * Interface CreditmemoValidatorInterface + */ +interface CreditmemoValidatorInterface +{ + /** + * @param CreditmemoInterface $entity + * @param ValidatorInterface[] $validators + * @return string[] + * @throws DocumentValidationException + */ + public function validate(CreditmemoInterface $entity, array $validators); +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Item/Validation/CreationQuantityValidator.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Item/Validation/CreationQuantityValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..edad1a2520632b31a89e355bb99913d0287df748 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Item/Validation/CreationQuantityValidator.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Model\Order\Creditmemo\Item\Validation; + +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\Data\OrderItemInterface; +use Magento\Sales\Api\OrderItemRepositoryInterface; +use Magento\Sales\Model\Order\Item; +use Magento\Sales\Model\ValidatorInterface; + +/** + * Class CreationQuantityValidator + */ +class CreationQuantityValidator implements ValidatorInterface +{ + /** + * @var OrderItemRepositoryInterface + */ + private $orderItemRepository; + + /** + * @var OrderInterfaceFactory + */ + private $context; + + /** + * ItemCreationQuantityValidator constructor. + * @param OrderItemRepositoryInterface $orderItemRepository + * @param object $context + */ + public function __construct(OrderItemRepositoryInterface $orderItemRepository, $context = null) + { + $this->orderItemRepository = $orderItemRepository; + $this->context = $context; + } + + /** + * @inheritdoc + */ + public function validate($entity) + { + try { + $orderItem = $this->orderItemRepository->get($entity->getOrderItemId()); + if (!$this->isItemPartOfContextOrder($orderItem)) { + return [__('The creditmemo contains product item that is not part of the original order.')]; + } + } catch (NoSuchEntityException $e) { + return [__('The creditmemo contains product item that is not part of the original order.')]; + } + + if (!$this->isQtyAvailable($orderItem, $entity->getQty())) { + return [__('The quantity to refund must not be greater than the unrefunded quantity.')]; + } + + return []; + } + + /** + * @param Item $orderItem + * @param int $qty + * @return bool + */ + private function isQtyAvailable(Item $orderItem, $qty) + { + return $qty <= $orderItem->getQtyToRefund() || $orderItem->isDummy(); + } + + /** + * @param OrderItemInterface $orderItem + * @return bool + */ + private function isItemPartOfContextOrder(OrderItemInterface $orderItem) + { + return $this->context instanceof OrderInterface && $this->context->getEntityId() === $orderItem->getOrderId(); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreation.php b/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreation.php new file mode 100644 index 0000000000000000000000000000000000000000..10e70402be6e4d4e6848f7f1065d44b22058a3d7 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreation.php @@ -0,0 +1,86 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Model\Order\Creditmemo; + +use Magento\Sales\Api\Data\CreditmemoItemCreationInterface; + +/** + * Class LineItem + */ +class ItemCreation implements CreditmemoItemCreationInterface +{ + /** + * @var int + */ + private $orderItemId; + + /** + * @var float + */ + private $qty; + + /** + * @var \Magento\Sales\Api\Data\CreditmemoItemCreationExtensionInterface + */ + private $extensionAttributes; + + /** + * {@inheritdoc} + */ + public function getOrderItemId() + { + return $this->orderItemId; + } + + /** + * {@inheritdoc} + */ + public function setOrderItemId($orderItemId) + { + $this->orderItemId = $orderItemId; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getQty() + { + return $this->qty; + } + + /** + * {@inheritdoc} + */ + public function setQty($qty) + { + $this->qty = $qty; + return $this; + } + + /** + * Retrieve existing extension attributes object or create a new one. + * + * @return \Magento\Sales\Api\Data\CreditmemoItemCreationExtensionInterface|null + */ + public function getExtensionAttributes() + { + return $this->extensionAttributes; + } + + /** + * Set an extension attributes object. + * + * @param \Magento\Sales\Api\Data\CreditmemoItemCreationExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Magento\Sales\Api\Data\CreditmemoItemCreationExtensionInterface $extensionAttributes + ) { + $this->extensionAttributes = $extensionAttributes; + return $this; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreationValidator.php b/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreationValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..f1b8e6d9cab9ba4324d8ca01ae1b9267f6e4c496 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreationValidator.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Model\Order\Creditmemo; + +use Magento\Sales\Api\Data\CreditmemoItemCreationInterface; +use Magento\Sales\Api\Data\OrderInterface; + +/** + * Class ItemCreationValidator + */ +class ItemCreationValidator implements ItemCreationValidatorInterface +{ + /** + * @var \Magento\Sales\Model\Validator + */ + private $validator; + + /** + * InvoiceValidatorRunner constructor. + * @param \Magento\Sales\Model\Validator $validator + */ + public function __construct(\Magento\Sales\Model\Validator $validator) + { + $this->validator = $validator; + } + + /** + * @inheritdoc + */ + public function validate( + CreditmemoItemCreationInterface $entity, + array $validators, + OrderInterface $context = null + ) { + return $this->validator->validate($entity, $validators, $context); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreationValidatorInterface.php b/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreationValidatorInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..9f8bb84ccd16a175ee23de755169281026dc053b --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreationValidatorInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Model\Order\Creditmemo; + +use Magento\Sales\Api\Data\CreditmemoItemCreationInterface; +use Magento\Sales\Api\Data\OrderInterface; + +/** + * Interface ItemCreationValidatorInterface + */ +interface ItemCreationValidatorInterface +{ + /** + * @param CreditmemoItemCreationInterface $item + * @param array $validators + * @param OrderInterface|null $context + * @return mixed + */ + public function validate(CreditmemoItemCreationInterface $item, array $validators, OrderInterface $context = null); +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Notifier.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Notifier.php new file mode 100644 index 0000000000000000000000000000000000000000..47dbecca6b59bdc727f1b58b18e3c88a73c5993f --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Notifier.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Model\Order\Creditmemo; + +/** + * CreditMemo notifier. + * + * @api + */ +class Notifier implements \Magento\Sales\Model\Order\Creditmemo\NotifierInterface +{ + /** + * @var \Magento\Sales\Model\Order\CreditMemo\SenderInterface[] + */ + private $senders; + + /** + * @param \Magento\Sales\Model\Order\CreditMemo\SenderInterface[] $senders + */ + public function __construct(array $senders = []) + { + $this->senders = $senders; + } + + /** + * {@inheritdoc} + */ + public function notify( + \Magento\Sales\Api\Data\OrderInterface $order, + \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + $forceSyncMode = false + ) { + foreach ($this->senders as $sender) { + $sender->send($order, $creditmemo, $comment, $forceSyncMode); + } + } +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/NotifierInterface.php b/app/code/Magento/Sales/Model/Order/Creditmemo/NotifierInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..ef42bd18633cfc21f0b92df28dde52438ed22c52 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/NotifierInterface.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Model\Order\Creditmemo; + +/** + * Interface for CreditMemo notifier. + * + * @api + */ +interface NotifierInterface +{ + /** + * Notifies customer. + * + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @param \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment + * @param bool $forceSyncMode + * + * @return void + */ + public function notify( + \Magento\Sales\Api\Data\OrderInterface $order, + \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + $forceSyncMode = false + ); +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/RefundOperation.php b/app/code/Magento/Sales/Model/Order/Creditmemo/RefundOperation.php index ef60ad2ce51bea1c2610f8ca6289d3cf4686c6c9..f01dd0dc3732e12a287306edd7ed173e604e9dc9 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/RefundOperation.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/RefundOperation.php @@ -114,9 +114,8 @@ class RefundOperation $order->getBaseTotalInvoicedCost() - $creditmemo->getBaseCost() ); - if ($online) { - $order->getPayment()->refund($creditmemo); - } + $creditmemo->setDoTransaction($online); + $order->getPayment()->refund($creditmemo); $this->eventManager->dispatch('sales_order_creditmemo_refund', ['creditmemo' => $creditmemo]); } diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php new file mode 100644 index 0000000000000000000000000000000000000000..76210505fd4672778cfae0742de64bc990ccac86 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php @@ -0,0 +1,149 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Model\Order\Creditmemo\Sender; + +use Magento\Sales\Model\Order\Email\Sender; +use Magento\Sales\Model\Order\Creditmemo\SenderInterface; + +/** + * Email notification sender for Creditmemo. + */ +class EmailSender extends Sender implements SenderInterface +{ + /** + * @var \Magento\Payment\Helper\Data + */ + private $paymentHelper; + + /** + * @var \Magento\Sales\Model\ResourceModel\Order\Creditmemo + */ + private $creditmemoResource; + + /** + * @var \Magento\Framework\App\Config\ScopeConfigInterface + */ + private $globalConfig; + + /** + * @var \Magento\Framework\Event\ManagerInterface + */ + private $eventManager; + + /** + * @param \Magento\Sales\Model\Order\Email\Container\Template $templateContainer + * @param \Magento\Sales\Model\Order\Email\Container\CreditmemoIdentity $identityContainer + * @param \Magento\Sales\Model\Order\Email\SenderBuilderFactory $senderBuilderFactory + * @param \Psr\Log\LoggerInterface $logger + * @param \Magento\Sales\Model\Order\Address\Renderer $addressRenderer + * @param \Magento\Payment\Helper\Data $paymentHelper + * @param \Magento\Sales\Model\ResourceModel\Order\Creditmemo $creditmemoResource + * @param \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig + * @param \Magento\Framework\Event\ManagerInterface $eventManager + */ + public function __construct( + \Magento\Sales\Model\Order\Email\Container\Template $templateContainer, + \Magento\Sales\Model\Order\Email\Container\CreditmemoIdentity $identityContainer, + \Magento\Sales\Model\Order\Email\SenderBuilderFactory $senderBuilderFactory, + \Psr\Log\LoggerInterface $logger, + \Magento\Sales\Model\Order\Address\Renderer $addressRenderer, + \Magento\Payment\Helper\Data $paymentHelper, + \Magento\Sales\Model\ResourceModel\Order\Creditmemo $creditmemoResource, + \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig, + \Magento\Framework\Event\ManagerInterface $eventManager + ) { + parent::__construct( + $templateContainer, + $identityContainer, + $senderBuilderFactory, + $logger, + $addressRenderer + ); + + $this->paymentHelper = $paymentHelper; + $this->creditmemoResource = $creditmemoResource; + $this->globalConfig = $globalConfig; + $this->eventManager = $eventManager; + } + + /** + * Sends order creditmemo email to the customer. + * + * Email will be sent immediately in two cases: + * + * - if asynchronous email sending is disabled in global settings + * - if $forceSyncMode parameter is set to TRUE + * + * Otherwise, email will be sent later during running of + * corresponding cron job. + * + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @param \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment + * @param bool $forceSyncMode + * + * @return bool + */ + public function send( + \Magento\Sales\Api\Data\OrderInterface $order, + \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + $forceSyncMode = false + ) { + $creditmemo->setSendEmail(true); + + if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { + $transport = [ + 'order' => $order, + 'creditmemo' => $creditmemo, + 'comment' => $comment ? $comment->getComment() : '', + 'billing' => $order->getBillingAddress(), + 'payment_html' => $this->getPaymentHtml($order), + 'store' => $order->getStore(), + 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), + 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + ]; + + $this->eventManager->dispatch( + 'email_creditmemo_set_template_vars_before', + ['sender' => $this, 'transport' => $transport] + ); + + $this->templateContainer->setTemplateVars($transport); + + if ($this->checkAndSend($order)) { + $creditmemo->setEmailSent(true); + + $this->creditmemoResource->saveAttribute($creditmemo, ['send_email', 'email_sent']); + + return true; + } + } else { + $creditmemo->setEmailSent(null); + + $this->creditmemoResource->saveAttribute($creditmemo, 'email_sent'); + } + + $this->creditmemoResource->saveAttribute($creditmemo, 'send_email'); + + return false; + } + + /** + * Returns payment info block as HTML. + * + * @param \Magento\Sales\Api\Data\OrderInterface $order + * + * @return string + */ + private function getPaymentHtml(\Magento\Sales\Api\Data\OrderInterface $order) + { + return $this->paymentHelper->getInfoBlockHtml( + $order->getPayment(), + $this->identityContainer->getStore()->getStoreId() + ); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/SenderInterface.php b/app/code/Magento/Sales/Model/Order/Creditmemo/SenderInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..00d316a8ec98aa79f3ef991b187e9070f2c15901 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/SenderInterface.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Model\Order\Creditmemo; + +/** + * Interface for notification sender for CreditMemo. + */ +interface SenderInterface +{ + /** + * Sends notification to a customer. + * + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @param \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment + * @param bool $forceSyncMode + * + * @return bool + */ + public function send( + \Magento\Sales\Api\Data\OrderInterface $order, + \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + $forceSyncMode = false + ); +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Validation/QuantityValidator.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Validation/QuantityValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..82fd0479166e5257730b9c0ec83079eb5d13b37e --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Validation/QuantityValidator.php @@ -0,0 +1,253 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Model\Order\Creditmemo\Validation; + +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderItemInterface; +use Magento\Sales\Api\Data\CreditmemoInterface; +use Magento\Sales\Api\InvoiceRepositoryInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Creditmemo; +use Magento\Sales\Model\Order\Item; +use Magento\Sales\Model\ValidatorInterface; + +/** + * Class QuantityValidator + */ +class QuantityValidator implements ValidatorInterface +{ + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var InvoiceRepositoryInterface + */ + private $invoiceRepository; + + /** + * @var \Magento\Framework\Pricing\PriceCurrencyInterface + */ + private $priceCurrency; + + /** + * InvoiceValidator constructor. + * + * @param OrderRepositoryInterface $orderRepository + * @param InvoiceRepositoryInterface $invoiceRepository + * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + */ + public function __construct( + OrderRepositoryInterface $orderRepository, + InvoiceRepositoryInterface $invoiceRepository, + \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + ) { + $this->orderRepository = $orderRepository; + $this->invoiceRepository = $invoiceRepository; + $this->priceCurrency = $priceCurrency; + } + + /** + * @inheritdoc + */ + public function validate($entity) + { + /** + * @var $entity CreditmemoInterface + */ + if ($entity->getOrderId() === null) { + return [__('Order Id is required for creditmemo document')]; + } + + $messages = []; + + $order = $this->orderRepository->get($entity->getOrderId()); + $orderItemsById = $this->getOrderItems($order); + $invoiceQtysRefundLimits = $this->getInvoiceQtysRefundLimits($entity, $order); + + $totalQuantity = 0; + foreach ($entity->getItems() as $item) { + if (!isset($orderItemsById[$item->getOrderItemId()])) { + $messages[] = __( + 'The creditmemo contains product SKU "%1" that is not part of the original order.', + $item->getSku() + ); + continue; + } + $orderItem = $orderItemsById[$item->getOrderItemId()]; + + if ( + !$this->canRefundItem($orderItem, $item->getQty(), $invoiceQtysRefundLimits) || + !$this->isQtyAvailable($orderItem, $item->getQty()) + ) { + $messages[] =__( + 'The quantity to creditmemo must not be greater than the unrefunded quantity' + . ' for product SKU "%1".', + $orderItem->getSku() + ); + } else { + $totalQuantity += $item->getQty(); + } + } + + if ($entity->getGrandTotal() <= 0) { + $messages[] = __('The credit memo\'s total must be positive.'); + } elseif ($totalQuantity <= 0 && !$this->canRefundShipping($order)) { + $messages[] = __('You can\'t create a creditmemo without products.'); + } + + return $messages; + } + + /** + * We can have problem with float in php (on some server $a=762.73;$b=762.73; $a-$b!=0) + * for this we have additional diapason for 0 + * TotalPaid - contains amount, that were not rounded. + * + * @param OrderInterface $order + * @return bool + */ + private function canRefundShipping(OrderInterface $order) + { + return !abs($this->priceCurrency->round($order->getShippingAmount()) - $order->getShippingRefunded()) < .0001; + } + + /** + * @param CreditmemoInterface $creditmemo + * @param OrderInterface $order + * @return array + */ + private function getInvoiceQtysRefundLimits(CreditmemoInterface $creditmemo, OrderInterface $order) + { + $invoiceQtysRefundLimits = []; + if ($creditmemo->getInvoiceId() !== null) { + $invoiceQtysRefunded = []; + $invoice = $this->invoiceRepository->get($creditmemo->getInvoiceId()); + foreach ($order->getCreditmemosCollection() as $createdCreditmemo) { + if ( + $createdCreditmemo->getState() != Creditmemo::STATE_CANCELED && + $createdCreditmemo->getInvoiceId() == $invoice->getId() + ) { + foreach ($createdCreditmemo->getAllItems() as $createdCreditmemoItem) { + $orderItemId = $createdCreditmemoItem->getOrderItem()->getId(); + if (isset($invoiceQtysRefunded[$orderItemId])) { + $invoiceQtysRefunded[$orderItemId] += $createdCreditmemoItem->getQty(); + } else { + $invoiceQtysRefunded[$orderItemId] = $createdCreditmemoItem->getQty(); + } + } + } + } + + foreach ($invoice->getItems() as $invoiceItem) { + $invoiceQtyCanBeRefunded = $invoiceItem->getQty(); + $orderItemId = $invoiceItem->getOrderItem()->getId(); + if (isset($invoiceQtysRefunded[$orderItemId])) { + $invoiceQtyCanBeRefunded = $invoiceQtyCanBeRefunded - $invoiceQtysRefunded[$orderItemId]; + } + $invoiceQtysRefundLimits[$orderItemId] = $invoiceQtyCanBeRefunded; + } + } + + return $invoiceQtysRefundLimits; + } + + /** + * @param OrderInterface $order + * @return OrderItemInterface[] + */ + private function getOrderItems(OrderInterface $order) + { + $orderItemsById = []; + foreach ($order->getItems() as $item) { + $orderItemsById[$item->getItemId()] = $item; + } + + return $orderItemsById; + } + + /** + * @param Item $orderItem + * @param int $qty + * @return bool + */ + private function isQtyAvailable(Item $orderItem, $qty) + { + return $qty <= $orderItem->getQtyToRefund() || $orderItem->isDummy(); + } + + /** + * Check if order item can be refunded + * + * @param \Magento\Sales\Model\Order\Item $item + * @param double $qty + * @param array $invoiceQtysRefundLimits + * @return bool + */ + private function canRefundItem(\Magento\Sales\Model\Order\Item $item, $qty, array $invoiceQtysRefundLimits) + { + if ($item->isDummy()) { + return $this->canRefundDummyItem($item, $qty, $invoiceQtysRefundLimits); + } + + return $this->canRefundNoDummyItem($item, $invoiceQtysRefundLimits); + } + + /** + * Check if no dummy order item can be refunded + * + * @param \Magento\Sales\Model\Order\Item $item + * @param array $invoiceQtysRefundLimits + * @return bool + */ + private function canRefundNoDummyItem(\Magento\Sales\Model\Order\Item $item, array $invoiceQtysRefundLimits = []) + { + if ($item->getQtyToRefund() < 0) { + return false; + } + if (isset($invoiceQtysRefundLimits[$item->getId()])) { + return $invoiceQtysRefundLimits[$item->getId()] > 0; + } + return true; + } + + /** + * @param Item $item + * @param int $qty + * @param array $invoiceQtysRefundLimits + * @return bool + */ + private function canRefundDummyItem(\Magento\Sales\Model\Order\Item $item, $qty, array $invoiceQtysRefundLimits) + { + if ($item->getHasChildren()) { + foreach ($item->getChildrenItems() as $child) { + if ($this->canRefundRequestedQty($child, $qty, $invoiceQtysRefundLimits)) { + return true; + } + } + } elseif ($item->getParentItem()) { + return $this->canRefundRequestedQty($item->getParentItem(), $qty, $invoiceQtysRefundLimits); + } + + return false; + } + + /** + * @param Item $item + * @param int $qty + * @param array $invoiceQtysRefundLimits + * @return bool + */ + private function canRefundRequestedQty( + \Magento\Sales\Model\Order\Item $item, + $qty, + array $invoiceQtysRefundLimits + ) { + return $qty === null ? $this->canRefundNoDummyItem($item, $invoiceQtysRefundLimits) : $qty > 0; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Validation/TotalsValidator.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Validation/TotalsValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..2cefc377b0674392b69c57d47d96ee418d57d788 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Validation/TotalsValidator.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Model\Order\Creditmemo\Validation; + +use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Sales\Model\ValidatorInterface; + +/** + * Class TotalsValidator + */ +class TotalsValidator implements ValidatorInterface +{ + /** + * @var PriceCurrencyInterface + */ + private $priceCurrency; + + /** + * TotalsValidator constructor. + * + * @param PriceCurrencyInterface $priceCurrency + */ + public function __construct(PriceCurrencyInterface $priceCurrency) + { + $this->priceCurrency = $priceCurrency; + } + + /** + * @inheritDoc + */ + public function validate($entity) + { + $messages = []; + $baseOrderRefund = $this->priceCurrency->round( + $entity->getOrder()->getBaseTotalRefunded() + $entity->getBaseGrandTotal() + ); + if ($baseOrderRefund > $this->priceCurrency->round($entity->getOrder()->getBaseTotalPaid())) { + $baseAvailableRefund = $entity->getOrder()->getBaseTotalPaid() + - $entity->getOrder()->getBaseTotalRefunded(); + + $messages[] = __( + 'The most money available to refund is %1.', + $baseAvailableRefund + ); + } + + return $messages; + } +} diff --git a/app/code/Magento/Sales/Model/Order/CreditmemoDocumentFactory.php b/app/code/Magento/Sales/Model/Order/CreditmemoDocumentFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..469b226053cdd4e2b0658dcd596d7d740572f0c7 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/CreditmemoDocumentFactory.php @@ -0,0 +1,151 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Model\Order; + +/** + * Class CreditmemoDocumentFactory + */ +class CreditmemoDocumentFactory +{ + /** + * @var \Magento\Sales\Model\Order\CreditmemoFactory + */ + private $creditmemoFactory; + + /** + * @var \Magento\Sales\Api\Data\CreditmemoCommentInterfaceFactory + */ + private $commentFactory; + + /** + * @var \Magento\Framework\EntityManager\HydratorPool + */ + private $hydratorPool; + + /** + * @var \Magento\Sales\Api\OrderRepositoryInterface + */ + private $orderRepository; + + /** + * CreditmemoDocumentFactory constructor. + * + * @param \Magento\Sales\Model\Order\CreditmemoFactory $creditmemoFactory + * @param \Magento\Sales\Api\Data\CreditmemoCommentInterfaceFactory $commentFactory + * @param \Magento\Framework\EntityManager\HydratorPool $hydratorPool + * @param \Magento\Sales\Api\OrderRepositoryInterface $orderRepository + */ + public function __construct( + \Magento\Sales\Model\Order\CreditmemoFactory $creditmemoFactory, + \Magento\Sales\Api\Data\CreditmemoCommentInterfaceFactory $commentFactory, + \Magento\Framework\EntityManager\HydratorPool $hydratorPool, + \Magento\Sales\Api\OrderRepositoryInterface $orderRepository + ) { + $this->creditmemoFactory = $creditmemoFactory; + $this->commentFactory = $commentFactory; + $this->hydratorPool = $hydratorPool; + $this->orderRepository = $orderRepository; + } + + /** + * Get array with original data for new Creditmemo document + * + * @param \Magento\Sales\Api\Data\CreditmemoItemCreationInterface[] $items + * @param \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface|null $arguments + * @return array + */ + private function getCreditmemoCreationData( + array $items = [], + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $data = ['qtys' => []]; + foreach ($items as $item) { + $data['qtys'][$item->getOrderItemId()] = $item->getQty(); + } + if ($arguments) { + $hydrator = $this->hydratorPool->getHydrator( + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface::class + ); + $data = array_merge($hydrator->extract($arguments), $data); + } + return $data; + } + + /** + * Attach comment to the Creditmemo document. + * + * @param \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment + * @param bool $appendComment + * @return \Magento\Sales\Api\Data\CreditmemoInterface + */ + private function attachComment( + \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment, + $appendComment = false + ) { + $commentData = $this->hydratorPool->getHydrator( + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface::class + )->extract($comment); + $comment = $this->commentFactory->create(['data' => $commentData]); + $comment->setParentId($creditmemo->getEntityId()) + ->setStoreId($creditmemo->getStoreId()) + ->setCreditmemo($creditmemo) + ->setIsCustomerNotified($appendComment); + $creditmemo->setComments([$comment]); + return $creditmemo; + + } + + /** + * Create new Creditmemo + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @param \Magento\Sales\Api\Data\CreditmemoItemCreationInterface[] $items + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment + * @param bool|null $appendComment + * @param \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface|null $arguments + * @return \Magento\Sales\Api\Data\CreditmemoInterface + */ + public function createFromOrder( + \Magento\Sales\Api\Data\OrderInterface $order, + array $items = [], + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $data = $this->getCreditmemoCreationData($items, $arguments); + $creditmemo = $this->creditmemoFactory->createByOrder($order, $data); + if ($comment) { + $creditmemo = $this->attachComment($creditmemo, $comment, $appendComment); + } + return $creditmemo; + } + + /** + * @param \Magento\Sales\Api\Data\InvoiceInterface $invoice + * @param \Magento\Sales\Api\Data\CreditmemoItemCreationInterface[] $items + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment + * @param bool|null $appendComment + * @param \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface|null $arguments + * @return \Magento\Sales\Api\Data\CreditmemoInterface + */ + public function createFromInvoice( + \Magento\Sales\Api\Data\InvoiceInterface $invoice, + array $items = [], + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $data = $this->getCreditmemoCreationData($items, $arguments); + /** @var $invoice \Magento\Sales\Model\Order\Invoice */ + $invoice->setOrder($this->orderRepository->get($invoice->getOrderId())); + $creditmemo = $this->creditmemoFactory->createByInvoice($invoice, $data); + if ($comment) { + $creditmemo = $this->attachComment($creditmemo, $comment, $appendComment); + } + return $creditmemo; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Validation/CanRefund.php b/app/code/Magento/Sales/Model/Order/Invoice/Validation/CanRefund.php new file mode 100644 index 0000000000000000000000000000000000000000..8e68cade3caa7e1a83ad1b3522eab2d43d2672cc --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Invoice/Validation/CanRefund.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Model\Order\Invoice\Validation; + +use Magento\Payment\Model\InfoInterface; +use Magento\Payment\Model\MethodInterface; +use Magento\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Api\OrderPaymentRepositoryInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order\Invoice; +use Magento\Sales\Model\ValidatorInterface; + +/** + * Class CanRefund + */ +class CanRefund implements ValidatorInterface +{ + /** + * @var OrderPaymentRepositoryInterface + */ + private $paymentRepository; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * CanRefund constructor. + * + * @param OrderPaymentRepositoryInterface $paymentRepository + * @param OrderRepositoryInterface $orderRepository + */ + public function __construct( + OrderPaymentRepositoryInterface $paymentRepository, + OrderRepositoryInterface $orderRepository + ) { + $this->paymentRepository = $paymentRepository; + $this->orderRepository = $orderRepository; + } + + /** + * @inheritdoc + */ + public function validate($entity) + { + if ( + $entity->getState() == Invoice::STATE_PAID && + $this->isGrandTotalEnoughToRefund($entity) && + $this->isPaymentAllowRefund($entity) + ) { + return []; + } + + return [__('We can\'t create creditmemo for the invoice.')]; + } + + /** + * @param InvoiceInterface $invoice + * @return bool + */ + private function isPaymentAllowRefund(InvoiceInterface $invoice) + { + $order = $this->orderRepository->get($invoice->getOrderId()); + $payment = $order->getPayment(); + if (!$payment instanceof InfoInterface) { + return false; + } + $method = $payment->getMethodInstance(); + return $this->canPartialRefund($method, $payment) || $this->canFullRefund($invoice, $method); + } + + /** + * @param InvoiceInterface $entity + * @return bool + */ + private function isGrandTotalEnoughToRefund(InvoiceInterface $entity) + { + return abs($entity->getBaseGrandTotal() - $entity->getBaseTotalRefunded()) >= .0001; + } + + /** + * @param MethodInterface $method + * @param InfoInterface $payment + * @return bool + */ + private function canPartialRefund(MethodInterface $method, InfoInterface $payment) + { + return $method->canRefund() && + $method->canRefundPartialPerInvoice() && + $payment->getAmountPaid() > $payment->getAmountRefunded(); + } + + /** + * @param InvoiceInterface $invoice + * @param MethodInterface $method + * @return bool + */ + private function canFullRefund(InvoiceInterface $invoice, MethodInterface $method) + { + return $method->canRefund() && !$invoice->getIsUsedForRefund(); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Validation/CanRefund.php b/app/code/Magento/Sales/Model/Order/Validation/CanRefund.php new file mode 100644 index 0000000000000000000000000000000000000000..c6fc1a0d705e8d49f49918f7919ec5cfaa3ee199 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Validation/CanRefund.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Model\Order\Validation; + +use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\ValidatorInterface; + +/** + * Class CanRefund + */ +class CanRefund implements ValidatorInterface +{ + /** + * @var PriceCurrencyInterface + */ + private $priceCurrency; + + /** + * CanRefund constructor. + * + * @param PriceCurrencyInterface $priceCurrency + */ + public function __construct(PriceCurrencyInterface $priceCurrency) + { + $this->priceCurrency = $priceCurrency; + } + + /** + * @inheritdoc + */ + public function validate($entity) + { + $messages = []; + if ($entity->getState() === Order::STATE_PAYMENT_REVIEW || + $entity->getState() === Order::STATE_HOLDED || + $entity->getState() === Order::STATE_CANCELED || + $entity->getState() === Order::STATE_CLOSED + ) { + $messages[] = __( + 'A creditmemo can not be created when an order has a status of %1', + $entity->getStatus() + ); + } elseif (!$this->isTotalPaidEnoughForRefund($entity)) { + $messages[] = __('The order does not allow a creditmemo to be created.'); + } + + return $messages; + } + + /** + * We can have problem with float in php (on some server $a=762.73;$b=762.73; $a-$b!=0) + * for this we have additional diapason for 0 + * TotalPaid - contains amount, that were not rounded. + * + * @param OrderInterface $order + * @return bool + */ + private function isTotalPaidEnoughForRefund(OrderInterface $order) + { + return !abs($this->priceCurrency->round($order->getTotalPaid()) - $order->getTotalRefunded()) < .0001; + } +} diff --git a/app/code/Magento/Sales/Model/RefundInvoice.php b/app/code/Magento/Sales/Model/RefundInvoice.php new file mode 100644 index 0000000000000000000000000000000000000000..37bf915aa505c6a757208b40a2c888515c6c8bc0 --- /dev/null +++ b/app/code/Magento/Sales/Model/RefundInvoice.php @@ -0,0 +1,252 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Model; + +use Magento\Framework\App\ResourceConnection; +use Magento\Sales\Api\CreditmemoRepositoryInterface; +use Magento\Sales\Api\InvoiceRepositoryInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Api\RefundInvoiceInterface; +use Magento\Sales\Model\Order\Config as OrderConfig; +use Magento\Sales\Model\Order\Creditmemo\CreditmemoValidatorInterface; +use Magento\Sales\Model\Order\Creditmemo\ItemCreationValidatorInterface; +use Magento\Sales\Model\Order\Creditmemo\NotifierInterface; +use Magento\Sales\Model\Order\Creditmemo\Item\Validation\CreationQuantityValidator; +use Magento\Sales\Model\Order\Creditmemo\Validation\QuantityValidator; +use Magento\Sales\Model\Order\Creditmemo\Validation\TotalsValidator; +use Magento\Sales\Model\Order\CreditmemoDocumentFactory; +use Magento\Sales\Model\Order\Invoice\InvoiceValidatorInterface; +use Magento\Sales\Model\Order\OrderStateResolverInterface; +use Magento\Sales\Model\Order\OrderValidatorInterface; +use Magento\Sales\Model\Order\PaymentAdapterInterface; +use Magento\Sales\Model\Order\Validation\CanRefund; +use Psr\Log\LoggerInterface; + +/** + * Class RefundInvoice + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class RefundInvoice implements RefundInvoiceInterface +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var OrderStateResolverInterface + */ + private $orderStateResolver; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var InvoiceRepositoryInterface + */ + private $invoiceRepository; + + /** + * @var OrderValidatorInterface + */ + private $orderValidator; + + /** + * @var InvoiceValidatorInterface + */ + private $invoiceValidator; + + /** + * @var CreditmemoValidatorInterface + */ + private $creditmemoValidator; + + /** + * @var ItemCreationValidatorInterface + */ + private $itemCreationValidator; + + /** + * @var CreditmemoRepositoryInterface + */ + private $creditmemoRepository; + + /** + * @var Order\PaymentAdapterInterface + */ + private $paymentAdapter; + + /** + * @var CreditmemoDocumentFactory + */ + private $creditmemoDocumentFactory; + + /** + * @var Order\Creditmemo\NotifierInterface + */ + private $notifier; + + /** + * @var OrderConfig + */ + private $config; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * RefundInvoice constructor. + * + * @param ResourceConnection $resourceConnection + * @param OrderStateResolverInterface $orderStateResolver + * @param OrderRepositoryInterface $orderRepository + * @param InvoiceRepositoryInterface $invoiceRepository + * @param OrderValidatorInterface $orderValidator + * @param InvoiceValidatorInterface $invoiceValidator + * @param CreditmemoValidatorInterface $creditmemoValidator + * @param Order\Creditmemo\ItemCreationValidatorInterface $itemCreationValidator + * @param CreditmemoRepositoryInterface $creditmemoRepository + * @param PaymentAdapterInterface $paymentAdapter + * @param CreditmemoDocumentFactory $creditmemoDocumentFactory + * @param NotifierInterface $notifier + * @param OrderConfig $config + * @param LoggerInterface $logger + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + ResourceConnection $resourceConnection, + OrderStateResolverInterface $orderStateResolver, + OrderRepositoryInterface $orderRepository, + InvoiceRepositoryInterface $invoiceRepository, + OrderValidatorInterface $orderValidator, + InvoiceValidatorInterface $invoiceValidator, + CreditmemoValidatorInterface $creditmemoValidator, + ItemCreationValidatorInterface $itemCreationValidator, + CreditmemoRepositoryInterface $creditmemoRepository, + PaymentAdapterInterface $paymentAdapter, + CreditmemoDocumentFactory $creditmemoDocumentFactory, + NotifierInterface $notifier, + OrderConfig $config, + LoggerInterface $logger + ) { + $this->resourceConnection = $resourceConnection; + $this->orderStateResolver = $orderStateResolver; + $this->orderRepository = $orderRepository; + $this->invoiceRepository = $invoiceRepository; + $this->orderValidator = $orderValidator; + $this->creditmemoValidator = $creditmemoValidator; + $this->itemCreationValidator = $itemCreationValidator; + $this->creditmemoRepository = $creditmemoRepository; + $this->paymentAdapter = $paymentAdapter; + $this->creditmemoDocumentFactory = $creditmemoDocumentFactory; + $this->notifier = $notifier; + $this->config = $config; + $this->logger = $logger; + $this->invoiceValidator = $invoiceValidator; + } + + /** + * @inheritdoc + */ + public function execute( + $invoiceId, + array $items = [], + $isOnline = false, + $notify = false, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $connection = $this->resourceConnection->getConnection('sales'); + $invoice = $this->invoiceRepository->get($invoiceId); + $order = $this->orderRepository->get($invoice->getOrderId()); + $creditmemo = $this->creditmemoDocumentFactory->createFromInvoice( + $invoice, + $items, + $comment, + ($appendComment && $notify), + $arguments + ); + $orderValidationResult = $this->orderValidator->validate( + $order, + [ + CanRefund::class + ] + ); + $invoiceValidationResult = $this->invoiceValidator->validate( + $invoice, + [ + \Magento\Sales\Model\Order\Invoice\Validation\CanRefund::class + ] + ); + $creditmemoValidationResult = $this->creditmemoValidator->validate( + $creditmemo, + [ + QuantityValidator::class, + TotalsValidator::class + ] + ); + $itemsValidation = []; + foreach ($items as $item) { + $itemsValidation = array_merge( + $itemsValidation, + $this->itemCreationValidator->validate( + $item, + [CreationQuantityValidator::class], + $order + ) + ); + } + $validationMessages = array_merge( + $orderValidationResult, + $invoiceValidationResult, + $creditmemoValidationResult, + $itemsValidation + ); + if (!empty($validationMessages )) { + throw new \Magento\Sales\Exception\DocumentValidationException( + __("Creditmemo Document Validation Error(s):\n" . implode("\n", $validationMessages)) + ); + } + $connection->beginTransaction(); + try { + $creditmemo->setState(\Magento\Sales\Model\Order\Creditmemo::STATE_REFUNDED); + $order = $this->paymentAdapter->refund($creditmemo, $order, $isOnline); + $order->setState( + $this->orderStateResolver->getStateForOrder($order, []) + ); + $order->setStatus($this->config->getStateDefaultStatus($order->getState())); + if (!$isOnline) { + $invoice->setIsUsedForRefund(true); + $invoice->setBaseTotalRefunded( + $invoice->getBaseTotalRefunded() + $creditmemo->getBaseGrandTotal() + ); + } + $this->invoiceRepository->save($invoice); + $order = $this->orderRepository->save($order); + $creditmemo = $this->creditmemoRepository->save($creditmemo); + $connection->commit(); + } catch (\Exception $e) { + $this->logger->critical($e); + $connection->rollBack(); + throw new \Magento\Sales\Exception\CouldNotRefundException( + __('Could not save a Creditmemo, see error log for details') + ); + } + if ($notify) { + if (!$appendComment) { + $comment = null; + } + $this->notifier->notify($order, $creditmemo, $comment); + } + + return $creditmemo->getEntityId(); + } +} diff --git a/app/code/Magento/Sales/Model/RefundOrder.php b/app/code/Magento/Sales/Model/RefundOrder.php new file mode 100644 index 0000000000000000000000000000000000000000..65d81df0d1de9fc4d2b2a7803369a0eda440ba12 --- /dev/null +++ b/app/code/Magento/Sales/Model/RefundOrder.php @@ -0,0 +1,214 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Model; + +use Magento\Framework\App\ResourceConnection; +use Magento\Sales\Api\CreditmemoRepositoryInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Api\RefundOrderInterface; +use Magento\Sales\Model\Order\Config as OrderConfig; +use Magento\Sales\Model\Order\Creditmemo\CreditmemoValidatorInterface; +use Magento\Sales\Model\Order\Creditmemo\ItemCreationValidatorInterface; +use Magento\Sales\Model\Order\Creditmemo\NotifierInterface; +use Magento\Sales\Model\Order\Creditmemo\Item\Validation\CreationQuantityValidator; +use Magento\Sales\Model\Order\Creditmemo\Validation\QuantityValidator; +use Magento\Sales\Model\Order\Creditmemo\Validation\TotalsValidator; +use Magento\Sales\Model\Order\CreditmemoDocumentFactory; +use Magento\Sales\Model\Order\OrderStateResolverInterface; +use Magento\Sales\Model\Order\OrderValidatorInterface; +use Magento\Sales\Model\Order\PaymentAdapterInterface; +use Magento\Sales\Model\Order\Validation\CanRefund; +use Psr\Log\LoggerInterface; + +/** + * Class RefundOrder + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class RefundOrder implements RefundOrderInterface +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var OrderStateResolverInterface + */ + private $orderStateResolver; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var OrderValidatorInterface + */ + private $orderValidator; + + /** + * @var CreditmemoValidatorInterface + */ + private $creditmemoValidator; + + /** + * @var Order\Creditmemo\ItemCreationValidatorInterface + */ + private $itemCreationValidator; + + /** + * @var CreditmemoRepositoryInterface + */ + private $creditmemoRepository; + + /** + * @var Order\PaymentAdapterInterface + */ + private $paymentAdapter; + + /** + * @var CreditmemoDocumentFactory + */ + private $creditmemoDocumentFactory; + + /** + * @var Order\Creditmemo\NotifierInterface + */ + private $notifier; + + /** + * @var OrderConfig + */ + private $config; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * RefundOrder constructor. + * @param ResourceConnection $resourceConnection + * @param OrderStateResolverInterface $orderStateResolver + * @param OrderRepositoryInterface $orderRepository + * @param OrderValidatorInterface $orderValidator + * @param CreditmemoValidatorInterface $creditmemoValidator + * @param ItemCreationValidatorInterface $itemCreationValidator + * @param CreditmemoRepositoryInterface $creditmemoRepository + * @param PaymentAdapterInterface $paymentAdapter + * @param CreditmemoDocumentFactory $creditmemoDocumentFactory + * @param NotifierInterface $notifier + * @param OrderConfig $config + * @param LoggerInterface $logger + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + ResourceConnection $resourceConnection, + OrderStateResolverInterface $orderStateResolver, + OrderRepositoryInterface $orderRepository, + OrderValidatorInterface $orderValidator, + CreditmemoValidatorInterface $creditmemoValidator, + ItemCreationValidatorInterface $itemCreationValidator, + CreditmemoRepositoryInterface $creditmemoRepository, + PaymentAdapterInterface $paymentAdapter, + CreditmemoDocumentFactory $creditmemoDocumentFactory, + NotifierInterface $notifier, + OrderConfig $config, + LoggerInterface $logger + ) { + $this->resourceConnection = $resourceConnection; + $this->orderStateResolver = $orderStateResolver; + $this->orderRepository = $orderRepository; + $this->orderValidator = $orderValidator; + $this->creditmemoValidator = $creditmemoValidator; + $this->itemCreationValidator = $itemCreationValidator; + $this->creditmemoRepository = $creditmemoRepository; + $this->paymentAdapter = $paymentAdapter; + $this->creditmemoDocumentFactory = $creditmemoDocumentFactory; + $this->notifier = $notifier; + $this->config = $config; + $this->logger = $logger; + } + + /** + * @inheritdoc + */ + public function execute( + $orderId, + array $items = [], + $notify = false, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $connection = $this->resourceConnection->getConnection('sales'); + $order = $this->orderRepository->get($orderId); + $creditmemo = $this->creditmemoDocumentFactory->createFromOrder( + $order, + $items, + $comment, + ($appendComment && $notify), + $arguments + ); + $orderValidationResult = $this->orderValidator->validate( + $order, + [ + CanRefund::class + ] + ); + $creditmemoValidationResult = $this->creditmemoValidator->validate( + $creditmemo, + [ + QuantityValidator::class, + TotalsValidator::class + ] + ); + $itemsValidation = []; + foreach ($items as $item) { + $itemsValidation = array_merge( + $itemsValidation, + $this->itemCreationValidator->validate( + $item, + [CreationQuantityValidator::class], + $order + ) + ); + } + $validationMessages = array_merge($orderValidationResult, $creditmemoValidationResult, $itemsValidation); + if (!empty($validationMessages)) { + throw new \Magento\Sales\Exception\DocumentValidationException( + __("Creditmemo Document Validation Error(s):\n" . implode("\n", $validationMessages)) + ); + } + $connection->beginTransaction(); + try { + $creditmemo->setState(\Magento\Sales\Model\Order\Creditmemo::STATE_REFUNDED); + $order = $this->paymentAdapter->refund($creditmemo, $order); + $order->setState( + $this->orderStateResolver->getStateForOrder($order, []) + ); + $order->setStatus($this->config->getStateDefaultStatus($order->getState())); + + $order = $this->orderRepository->save($order); + $creditmemo = $this->creditmemoRepository->save($creditmemo); + $connection->commit(); + } catch (\Exception $e) { + $this->logger->critical($e); + $connection->rollBack(); + throw new \Magento\Sales\Exception\CouldNotRefundException( + __('Could not save a Creditmemo, see error log for details') + ); + } + if ($notify) { + if (!$appendComment) { + $comment = null; + } + $this->notifier->notify($order, $creditmemo, $comment); + } + + return $creditmemo->getEntityId(); + } +} diff --git a/app/code/Magento/Sales/Model/Validator.php b/app/code/Magento/Sales/Model/Validator.php index b8d57ded2970209d5d2184945fbb22f13de47dbd..10aa0735b952e99e0d889b27ff378411d0d1d6d1 100644 --- a/app/code/Magento/Sales/Model/Validator.php +++ b/app/code/Magento/Sales/Model/Validator.php @@ -33,14 +33,20 @@ class Validator /** * @param object $entity * @param ValidatorInterface[] $validators - * @return string[] + * @param object|null $context + * @return \string[] * @throws ConfigurationMismatchException */ - public function validate($entity, array $validators) + public function validate($entity, array $validators, $context = null) { $messages = []; + $validatorArguments = []; + if ($context !== null) { + $validatorArguments['context'] = $context; + } + foreach ($validators as $validatorName) { - $validator = $this->objectManager->get($validatorName); + $validator = $this->objectManager->create($validatorName, $validatorArguments); if (!$validator instanceof ValidatorInterface) { throw new ConfigurationMismatchException( __( diff --git a/app/code/Magento/Sales/Model/ValidatorInterface.php b/app/code/Magento/Sales/Model/ValidatorInterface.php index 1882782e314f7b47b507c042697f223eadbf36b7..4489af44f40361f036e92671f776af479e45a4d7 100644 --- a/app/code/Magento/Sales/Model/ValidatorInterface.php +++ b/app/code/Magento/Sales/Model/ValidatorInterface.php @@ -15,7 +15,7 @@ interface ValidatorInterface { /** * @param object $entity - * @return array + * @return \Magento\Framework\Phrase[] * @throws DocumentValidationException * @throws NoSuchEntityException */ diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Item/Validation/CreateQuantityValidatorTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Item/Validation/CreateQuantityValidatorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1466a0f4fc9fe8d44841bcf4d31d4a34ab8abe44 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Item/Validation/CreateQuantityValidatorTest.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Test\Unit\Model\Order\Creditmemo\Item\Validation; + +use Magento\Sales\Api\OrderItemRepositoryInterface; +use Magento\Sales\Model\Order\Creditmemo\Item\Validation\CreationQuantityValidator; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\Order\Item; + +/** + * Class CreateQuantityValidatorTest + */ +class CreateQuantityValidatorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var OrderItemRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderItemRepositoryMock; + + /** + * @var Item|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderItemMock; + + /** + * @var CreationQuantityValidator|\PHPUnit_Framework_MockObject_MockObject + */ + private $createQuantityValidator; + + /** + * @var OrderInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $contexMock; + + /** + * @var \stdClass|\PHPUnit_Framework_MockObject_MockObject + */ + private $entity; + + protected function setUp() + { + $this->orderItemRepositoryMock = $this->getMockBuilder(OrderItemRepositoryInterface::class) + ->disableOriginalConstructor() + ->setMethods(['get']) + ->getMockForAbstractClass(); + + $this->orderItemMock = $this->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->entity = $this->getMockBuilder(\stdClass::class) + ->disableOriginalConstructor() + ->setMethods(['getOrderItemId', 'getQty']) + ->getMock(); + } + + /** + * @dataProvider dataProvider + */ + public function testValidateCreditMemoProductItems($orderItemId, $expectedResult, $withContext = false) + { + if ($orderItemId) { + $this->entity->expects($this->once()) + ->method('getOrderItemId') + ->willReturn($orderItemId); + + $this->orderItemRepositoryMock->expects($this->once()) + ->method('get') + ->with($orderItemId) + ->willReturn($this->orderItemMock); + } else { + $this->entity->expects($this->once()) + ->method('getOrderItemId') + ->willThrowException(new NoSuchEntityException()); + } + + $this->contexMock = null; + if ($withContext) { + $this->contexMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->entity->expects($this->once()) + ->method('getQty') + ->willReturn(11); + } + + $this->createQuantityValidator = new CreationQuantityValidator( + $this->orderItemRepositoryMock, + $this->contexMock + ); + + $this->assertEquals($expectedResult, $this->createQuantityValidator->validate($this->entity)); + } + + public function dataProvider() + { + return [ + 'testValidateCreditMemoProductItems' => [ + 1, + [__('The creditmemo contains product item that is not part of the original order.')], + ], + 'testValidateWithException' => [ + null, + [__('The creditmemo contains product item that is not part of the original order.')] + ], + 'testValidateWithContext' => [ + 1, + [__('The quantity to refund must not be greater than the unrefunded quantity.')], + true + ], + ]; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/RefundOperationTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/RefundOperationTest.php index 1b8d40d0427165f5c8587deb422b1ac9e3f4dbd2..2c5173507d997797c8d47cef8d7c70a6ceb56370 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/RefundOperationTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/RefundOperationTest.php @@ -50,7 +50,7 @@ class RefundOperationTest extends \PHPUnit_Framework_TestCase $this->creditmemoMock = $this->getMockBuilder(\Magento\Sales\Api\Data\CreditmemoInterface::class) ->disableOriginalConstructor() - ->setMethods(['getBaseCost']) + ->setMethods(['getBaseCost', 'setDoTransaction']) ->getMockForAbstractClass(); $this->paymentMock = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class) @@ -142,6 +142,7 @@ class RefundOperationTest extends \PHPUnit_Framework_TestCase public function testExecuteOffline($amounts) { $orderId = 1; + $online = false; $this->creditmemoMock->expects($this->once()) ->method('getState') ->willReturn(Creditmemo::STATE_REFUNDED); @@ -174,8 +175,17 @@ class RefundOperationTest extends \PHPUnit_Framework_TestCase $this->orderMock->expects($this->never()) ->method('setTotalOnlineRefunded'); - $this->orderMock->expects($this->never()) - ->method('getPayment'); + $this->orderMock->expects($this->once()) + ->method('getPayment') + ->willReturn($this->paymentMock); + + $this->paymentMock->expects($this->once()) + ->method('refund') + ->with($this->creditmemoMock); + + $this->creditmemoMock->expects($this->once()) + ->method('setDoTransaction') + ->with($online); $this->eventManagerMock->expects($this->once()) ->method('dispatch') @@ -186,7 +196,7 @@ class RefundOperationTest extends \PHPUnit_Framework_TestCase $this->assertEquals( $this->orderMock, - $this->subject->execute($this->creditmemoMock, $this->orderMock, false) + $this->subject->execute($this->creditmemoMock, $this->orderMock, $online) ); } @@ -197,6 +207,7 @@ class RefundOperationTest extends \PHPUnit_Framework_TestCase public function testExecuteOnline($amounts) { $orderId = 1; + $online = true; $this->creditmemoMock->expects($this->once()) ->method('getState') ->willReturn(Creditmemo::STATE_REFUNDED); @@ -229,6 +240,10 @@ class RefundOperationTest extends \PHPUnit_Framework_TestCase $this->orderMock->expects($this->never()) ->method('setTotalOfflineRefunded'); + $this->creditmemoMock->expects($this->once()) + ->method('setDoTransaction') + ->with($online); + $this->orderMock->expects($this->once()) ->method('getPayment') ->willReturn($this->paymentMock); @@ -238,7 +253,7 @@ class RefundOperationTest extends \PHPUnit_Framework_TestCase $this->assertEquals( $this->orderMock, - $this->subject->execute($this->creditmemoMock, $this->orderMock, true) + $this->subject->execute($this->creditmemoMock, $this->orderMock, $online) ); } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d1fe28b21b59b47558c0e44d0b55eacc0f6b847c --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php @@ -0,0 +1,361 @@ +<?php + +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Test\Unit\Model\Order\Creditmemo\Sender; + +/** + * Unit test for email notification sender for Creditmemo. + * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class EmailSenderTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Sales\Model\Order\Creditmemo\Sender\EmailSender + */ + private $subject; + + /** + * @var \Magento\Sales\Model\Order|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderMock; + + /** + * @var \Magento\Store\Model\Store|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeMock; + + /** + * @var \Magento\Sales\Model\Order\Email\Sender|\PHPUnit_Framework_MockObject_MockObject + */ + private $senderMock; + + /** + * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $loggerMock; + + /** + * @var \Magento\Sales\Api\Data\CreditmemoInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoMock; + + /** + * @var \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $commentMock; + + /** + * @var \Magento\Sales\Model\Order\Address|\PHPUnit_Framework_MockObject_MockObject + */ + private $addressMock; + + /** + * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $globalConfigMock; + + /** + * @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $eventManagerMock; + + /** + * @var \Magento\Payment\Model\Info|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentInfoMock; + + /** + * @var \Magento\Payment\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentHelperMock; + + /** + * @var \Magento\Sales\Model\ResourceModel\Order\Creditmemo|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoResourceMock; + + /** + * @var \Magento\Sales\Model\Order\Address\Renderer|\PHPUnit_Framework_MockObject_MockObject + */ + private $addressRendererMock; + + /** + * @var \Magento\Sales\Model\Order\Email\Container\Template|\PHPUnit_Framework_MockObject_MockObject + */ + private $templateContainerMock; + + /** + * @var \Magento\Sales\Model\Order\Email\Container\CreditmemoIdentity|\PHPUnit_Framework_MockObject_MockObject + */ + private $identityContainerMock; + + /** + * @var \Magento\Sales\Model\Order\Email\SenderBuilderFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $senderBuilderFactoryMock; + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + protected function setUp() + { + $this->orderMock = $this->getMockBuilder(\Magento\Sales\Model\Order::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->setMethods(['getStoreId']) + ->disableOriginalConstructor() + ->getMock(); + + $this->storeMock->expects($this->any()) + ->method('getStoreId') + ->willReturn(1); + $this->orderMock->expects($this->any()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->senderMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Email\Sender::class) + ->disableOriginalConstructor() + ->setMethods(['send', 'sendCopyTo']) + ->getMock(); + + $this->loggerMock = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->creditmemoMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Creditmemo::class) + ->disableOriginalConstructor() + ->setMethods(['setSendEmail', 'setEmailSent']) + ->getMock(); + + $this->commentMock = $this->getMockBuilder(\Magento\Sales\Api\Data\CreditmemoCommentCreationInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->commentMock->expects($this->any()) + ->method('getComment') + ->willReturn('Comment text'); + + $this->addressMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Address::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderMock->expects($this->any()) + ->method('getBillingAddress') + ->willReturn($this->addressMock); + $this->orderMock->expects($this->any()) + ->method('getShippingAddress') + ->willReturn($this->addressMock); + + $this->globalConfigMock = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->paymentInfoMock = $this->getMockBuilder(\Magento\Payment\Model\Info::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderMock->expects($this->any()) + ->method('getPayment') + ->willReturn($this->paymentInfoMock); + + $this->paymentHelperMock = $this->getMockBuilder(\Magento\Payment\Helper\Data::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->paymentHelperMock->expects($this->any()) + ->method('getInfoBlockHtml') + ->with($this->paymentInfoMock, 1) + ->willReturn('Payment Info Block'); + + $this->creditmemoResourceMock = $this->getMockBuilder( + \Magento\Sales\Model\ResourceModel\Order\Creditmemo::class + )->disableOriginalConstructor() + ->getMock(); + + $this->addressRendererMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Address\Renderer::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->addressRendererMock->expects($this->any()) + ->method('format') + ->with($this->addressMock, 'html') + ->willReturn('Formatted address'); + + $this->templateContainerMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Email\Container\Template::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->identityContainerMock = $this->getMockBuilder( + \Magento\Sales\Model\Order\Email\Container\CreditmemoIdentity::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->identityContainerMock->expects($this->any()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->senderBuilderFactoryMock = $this->getMockBuilder( + \Magento\Sales\Model\Order\Email\SenderBuilderFactory::class + ) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->subject = new \Magento\Sales\Model\Order\Creditmemo\Sender\EmailSender( + $this->templateContainerMock, + $this->identityContainerMock, + $this->senderBuilderFactoryMock, + $this->loggerMock, + $this->addressRendererMock, + $this->paymentHelperMock, + $this->creditmemoResourceMock, + $this->globalConfigMock, + $this->eventManagerMock + ); + } + + /** + * @param int $configValue + * @param bool $forceSyncMode + * @param bool $isComment + * @param bool $emailSendingResult + * + * @dataProvider sendDataProvider + * + * @return void + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testSend($configValue, $forceSyncMode, $isComment, $emailSendingResult) + { + $this->globalConfigMock->expects($this->once()) + ->method('getValue') + ->with('sales_email/general/async_sending') + ->willReturn($configValue); + + if (!$isComment) { + $this->commentMock = null; + } + + $this->creditmemoMock->expects($this->once()) + ->method('setSendEmail') + ->with(true); + + if (!$configValue || $forceSyncMode) { + $transport = [ + 'order' => $this->orderMock, + 'creditmemo' => $this->creditmemoMock, + 'comment' => $isComment ? 'Comment text' : '', + 'billing' => $this->addressMock, + 'payment_html' => 'Payment Info Block', + 'store' => $this->storeMock, + 'formattedShippingAddress' => 'Formatted address', + 'formattedBillingAddress' => 'Formatted address', + ]; + + $this->eventManagerMock->expects($this->once()) + ->method('dispatch') + ->with( + 'email_creditmemo_set_template_vars_before', + [ + 'sender' => $this->subject, + 'transport' => $transport, + ] + ); + + $this->templateContainerMock->expects($this->once()) + ->method('setTemplateVars') + ->with($transport); + + $this->identityContainerMock->expects($this->once()) + ->method('isEnabled') + ->willReturn($emailSendingResult); + + if ($emailSendingResult) { + $this->senderBuilderFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->senderMock); + + $this->senderMock->expects($this->once()) + ->method('send'); + + $this->senderMock->expects($this->once()) + ->method('sendCopyTo'); + + $this->creditmemoMock->expects($this->once()) + ->method('setEmailSent') + ->with(true); + + $this->creditmemoResourceMock->expects($this->once()) + ->method('saveAttribute') + ->with($this->creditmemoMock, ['send_email', 'email_sent']); + + $this->assertTrue( + $this->subject->send( + $this->orderMock, + $this->creditmemoMock, + $this->commentMock, + $forceSyncMode + ) + ); + } else { + $this->creditmemoResourceMock->expects($this->once()) + ->method('saveAttribute') + ->with($this->creditmemoMock, 'send_email'); + + $this->assertFalse( + $this->subject->send( + $this->orderMock, + $this->creditmemoMock, + $this->commentMock, + $forceSyncMode + ) + ); + } + } else { + $this->creditmemoMock->expects($this->once()) + ->method('setEmailSent') + ->with(null); + + $this->creditmemoResourceMock->expects($this->at(0)) + ->method('saveAttribute') + ->with($this->creditmemoMock, 'email_sent'); + $this->creditmemoResourceMock->expects($this->at(1)) + ->method('saveAttribute') + ->with($this->creditmemoMock, 'send_email'); + + $this->assertFalse( + $this->subject->send( + $this->orderMock, + $this->creditmemoMock, + $this->commentMock, + $forceSyncMode + ) + ); + } + } + + /** + * @return array + */ + public function sendDataProvider() + { + return [ + 'Successful sync sending with comment' => [0, false, true, true], + 'Successful sync sending without comment' => [0, false, false, true], + 'Failed sync sending with comment' => [0, false, true, false], + 'Successful forced sync sending with comment' => [1, true, true, true], + 'Async sending' => [1, false, false, false], + ]; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Validation/QuantityValidatorTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Validation/QuantityValidatorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..838c062956c24ac900835e53e746998f1de1271d --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Validation/QuantityValidatorTest.php @@ -0,0 +1,247 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Test\Unit\Model\Order\Creditmemo\Validation; + +use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Sales\Api\Data\CreditmemoInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\InvoiceRepositoryInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order\Creditmemo\Validation\QuantityValidator; + +/** + * Class QuantityValidatorTest + */ +class QuantityValidatorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var OrderRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderRepositoryMock; + + /** + * @var InvoiceRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $invoiceRepositoryMock; + + /** + * @var QuantityValidator + */ + private $validator; + + /** + * @var PriceCurrencyInterface + */ + private $priceCurrencyMock; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->invoiceRepositoryMock = $this->getMockBuilder(InvoiceRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->priceCurrencyMock = $this->getMockBuilder(PriceCurrencyInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->validator = new QuantityValidator( + $this->orderRepositoryMock, + $this->invoiceRepositoryMock, + $this->priceCurrencyMock + ); + } + + public function testValidateWithoutItems() + { + $creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $creditmemoMock->expects($this->exactly(2))->method('getOrderId') + ->willReturn(1); + $creditmemoMock->expects($this->once())->method('getItems') + ->willReturn([]); + $orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $orderMock->expects($this->once())->method('getItems') + ->willReturn([]); + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->with(1) + ->willReturn($orderMock); + $creditmemoMock->expects($this->once())->method('getGrandTotal') + ->willReturn(0); + $this->assertEquals( + [ + __('The credit memo\'s total must be positive.') + ], + $this->validator->validate($creditmemoMock) + ); + } + + public function testValidateWithoutOrder() + { + $creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $creditmemoMock->expects($this->once())->method('getOrderId') + ->willReturn(null); + $creditmemoMock->expects($this->never())->method('getItems'); + $this->assertEquals( + [__('Order Id is required for creditmemo document')], + $this->validator->validate($creditmemoMock) + ); + } + + public function testValidateWithWrongItemId() + { + $orderId = 1; + $orderItemId = 1; + $creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $creditmemoMock->expects($this->exactly(2))->method('getOrderId') + ->willReturn($orderId); + $creditmemoItemMock = $this->getMockBuilder( + \Magento\Sales\Api\Data\CreditmemoItemInterface::class + )->disableOriginalConstructor() + ->getMockForAbstractClass(); + $creditmemoItemMock->expects($this->once())->method('getOrderItemId') + ->willReturn($orderItemId); + $creditmemoItemSku = 'sku'; + $creditmemoItemMock->expects($this->once())->method('getSku') + ->willReturn($creditmemoItemSku); + $creditmemoMock->expects($this->exactly(1))->method('getItems') + ->willReturn([$creditmemoItemMock]); + + $orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $orderMock->expects($this->once())->method('getItems') + ->willReturn([]); + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->with($orderId) + ->willReturn($orderMock); + $creditmemoMock->expects($this->once())->method('getGrandTotal') + ->willReturn(12); + + $this->assertEquals( + [ + __( + 'The creditmemo contains product SKU "%1" that is not part of the original order.', + $creditmemoItemSku + ), + __('You can\'t create a creditmemo without products.') + ], + $this->validator->validate($creditmemoMock) + ); + } + + /** + * @param int $orderId + * @param int $orderItemId + * @param int $qtyToRequest + * @param int $qtyToRefund + * @param string $sku + * @param array $expected + * @dataProvider dataProviderForValidateQty + */ + public function testValidate($orderId, $orderItemId, $qtyToRequest, $qtyToRefund, $sku, $total, array $expected) + { + $creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $creditmemoMock->expects($this->exactly(2))->method('getOrderId') + ->willReturn($orderId); + $creditmemoMock->expects($this->once())->method('getGrandTotal') + ->willReturn($total); + $creditmemoItemMock = $this->getMockBuilder( + \Magento\Sales\Api\Data\CreditmemoItemInterface::class + )->disableOriginalConstructor() + ->getMockForAbstractClass(); + $creditmemoItemMock->expects($this->exactly(2))->method('getOrderItemId') + ->willReturn($orderItemId); + $creditmemoItemMock->expects($this->never())->method('getSku') + ->willReturn($sku); + $creditmemoItemMock->expects($this->atLeastOnce())->method('getQty') + ->willReturn($qtyToRequest); + $creditmemoMock->expects($this->exactly(1))->method('getItems') + ->willReturn([$creditmemoItemMock]); + + $orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) + ->disableOriginalConstructor() + ->getMock(); + $orderItemMock->expects($this->exactly(2))->method('getQtyToRefund') + ->willReturn($qtyToRefund); + $creditmemoItemMock->expects($this->any())->method('getQty') + ->willReturn($qtyToRequest); + $orderMock->expects($this->once())->method('getItems') + ->willReturn([$orderItemMock]); + $orderItemMock->expects($this->once())->method('getItemId') + ->willReturn($orderItemId); + $orderItemMock->expects($this->any())->method('getSku') + ->willReturn($sku); + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->with($orderId) + ->willReturn($orderMock); + + $this->assertEquals( + $expected, + $this->validator->validate($creditmemoMock) + ); + } + + /** + * @return array + */ + public function dataProviderForValidateQty() + { + $sku = 'sku'; + + return [ + [ + 'orderId' => 1, + 'orderItemId' => 1, + 'qtyToRequest' => 1, + 'qtyToRefund' => 1, + 'sku', + 'total' => 15, + 'expected' => [] + ], + [ + 'orderId' => 1, + 'orderItemId' => 1, + 'qtyToRequest' => 2, + 'qtyToRefund' => 1, + 'sku', + 'total' => 0, + 'expected' => [ + __( + 'The quantity to creditmemo must not be greater than the unrefunded quantity' + . ' for product SKU "%1".', + $sku + ), + __('The credit memo\'s total must be positive.') + ] + ], + ]; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoDocumentFactoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoDocumentFactoryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..521167f10aebd0993adebfef05e88f4b28ef1f54 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoDocumentFactoryTest.php @@ -0,0 +1,265 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Test\Unit\Model\Order; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order\CreditmemoDocumentFactory; +use Magento\Sales\Api\Data\CreditmemoCommentInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Invoice; +use Magento\Sales\Api\Data\CreditmemoInterface; +use Magento\Sales\Api\Data\CreditmemoItemCreationInterface; +use Magento\Sales\Api\Data\CreditmemoCommentCreationInterface; +use Magento\Framework\EntityManager\HydratorPool; +use Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface; +use Magento\Sales\Model\Order\CreditmemoFactory; +use Magento\Framework\EntityManager\HydratorInterface; + +/** + * Class CreditmemoDocumentFactoryTest + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CreditmemoDocumentFactoryTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var CreditmemoDocumentFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $factory; + + /** + * @var CreditmemoFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoFactoryMock; + + /** + * @var \Magento\Sales\Api\Data\CreditmemoCommentInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $commentFactoryMock; + + /** + * @var HydratorPool|\PHPUnit_Framework_MockObject_MockObject + */ + private $hydratorPoolMock; + + /** + * @var HydratorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $hydratorMock; + + /** + * @var \Magento\Sales\Model\Order|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderMock; + + /** + * @var \Magento\Sales\Model\Order\Invoice|\PHPUnit_Framework_MockObject_MockObject + */ + private $invoiceMock; + + /** + * @var CreditmemoItemCreationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoItemCreationMock; + + /** + * @var CreditmemoCommentCreationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $commentCreationMock; + + /** + * @var CreditmemoCreationArgumentsInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $commentCreationArgumentsMock; + + /** + * @var CreditmemoInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoMock; + + /** + * @var CreditmemoCommentInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $commentMock; + + /** + * @var OrderRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderRepositoryMock; + + public function setUp() + { + $this->objectManager = new ObjectManager($this); + $this->creditmemoFactoryMock = $this->getMockBuilder(CreditmemoFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->commentFactoryMock = + $this->getMockBuilder('Magento\Sales\Api\Data\CreditmemoCommentInterfaceFactory') + ->setMethods(['create']) + ->getMock(); + $this->hydratorPoolMock = $this->getMockBuilder(HydratorPool::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderMock = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->getMock(); + $this->invoiceMock = $this->getMockBuilder(Invoice::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoItemCreationMock = $this->getMockBuilder(CreditmemoItemCreationInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->hydratorMock = $this->getMockBuilder(HydratorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->commentCreationArgumentsMock = $this->getMockBuilder(CreditmemoCreationArgumentsInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->commentCreationMock = $this->getMockBuilder(CreditmemoCommentCreationInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoMock->expects($this->once()) + ->method('getEntityId') + ->willReturn(11); + + $this->commentMock = $this->getMockBuilder(CreditmemoCommentInterface::class) + ->disableOriginalConstructor() + ->setMethods( + array_merge( + get_class_methods(CreditmemoCommentInterface::class), + ['setStoreId', 'setCreditmemo'] + ) + ) + ->getMock(); + $this->factory = $this->objectManager->getObject( + CreditmemoDocumentFactory::class, + [ + 'creditmemoFactory' => $this->creditmemoFactoryMock, + 'commentFactory' => $this->commentFactoryMock, + 'hydratorPool' => $this->hydratorPoolMock, + 'orderRepository' => $this->orderRepositoryMock + ] + ); + } + + private function commonFactoryFlow() + { + $this->creditmemoItemCreationMock->expects($this->once()) + ->method('getOrderItemId') + ->willReturn(7); + $this->creditmemoItemCreationMock->expects($this->once()) + ->method('getQty') + ->willReturn(3); + $this->hydratorPoolMock->expects($this->exactly(2)) + ->method('getHydrator') + ->willReturnMap( + [ + [CreditmemoCreationArgumentsInterface::class, $this->hydratorMock], + [CreditmemoCommentCreationInterface::class, $this->hydratorMock], + ] + ); + $this->hydratorMock->expects($this->exactly(2)) + ->method('extract') + ->willReturnMap([ + [$this->commentCreationArgumentsMock, ['shipping_amount' => '20.00']], + [$this->commentCreationMock, ['comment' => 'text']] + ]); + $this->commentFactoryMock->expects($this->once()) + ->method('create') + ->with( + [ + 'data' => [ + 'comment' => 'text' + ] + ] + ) + ->willReturn($this->commentMock); + $this->creditmemoMock->expects($this->once()) + ->method('getEntityId') + ->willReturn(11); + $this->creditmemoMock->expects($this->once()) + ->method('getStoreId') + ->willReturn(1); + $this->commentMock->expects($this->once()) + ->method('setParentId') + ->with(11) + ->willReturnSelf(); + $this->commentMock->expects($this->once()) + ->method('setStoreId') + ->with(1) + ->willReturnSelf(); + $this->commentMock->expects($this->once()) + ->method('setIsCustomerNotified') + ->with(true) + ->willReturnSelf(); + $this->commentMock->expects($this->once()) + ->method('setCreditmemo') + ->with($this->creditmemoMock) + ->willReturnSelf(); + } + + public function testCreateFromOrder() + { + $this->commonFactoryFlow(); + $this->creditmemoFactoryMock->expects($this->once()) + ->method('createByOrder') + ->with( + $this->orderMock, + [ + 'shipping_amount' => '20.00', + 'qtys' => [7 => 3] + ] + ) + ->willReturn($this->creditmemoMock); + $this->factory->createFromOrder( + $this->orderMock, + [$this->creditmemoItemCreationMock], + $this->commentCreationMock, + true, + $this->commentCreationArgumentsMock + ); + } + + public function testCreateFromInvoice() + { + $this->commonFactoryFlow(); + $this->creditmemoFactoryMock->expects($this->once()) + ->method('createByInvoice') + ->with( + $this->invoiceMock, + [ + 'shipping_amount' => '20.00', + 'qtys' => [7 => 3] + ] + ) + ->willReturn($this->creditmemoMock); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + $this->invoiceMock->expects($this->once()) + ->method('setOrder') + ->with($this->orderMock) + ->willReturnSelf(); + $this->factory->createFromInvoice( + $this->invoiceMock, + [$this->creditmemoItemCreationMock], + $this->commentCreationMock, + true, + $this->commentCreationArgumentsMock + ); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Validation/CanRefundTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Validation/CanRefundTest.php new file mode 100644 index 0000000000000000000000000000000000000000..773f3b75c91f5c4f6aad03576b0cfe9dd4746ab1 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Validation/CanRefundTest.php @@ -0,0 +1,131 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Test\Unit\Model\Order\Invoice\Validation; + +use Magento\Sales\Api\Data\OrderInterface; + +/** + * Class CanRefundTest + */ +class CanRefundTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Sales\Model\Order\Invoice|\PHPUnit_Framework_MockObject_MockObject + */ + private $invoiceMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $orderPaymentRepositoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $orderRepositoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $paymentMock; + + /** + * @var \Magento\Sales\Model\Order\Invoice\Validation\CanRefund + */ + private $validator; + + protected function setUp() + { + $this->invoiceMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Invoice::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderPaymentRepositoryMock = $this->getMockBuilder( + \Magento\Sales\Api\OrderPaymentRepositoryInterface::class + ) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->orderRepositoryMock = $this->getMockBuilder(\Magento\Sales\Api\OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->paymentMock = $this->getMockBuilder(\Magento\Payment\Model\InfoInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->validator = new \Magento\Sales\Model\Order\Invoice\Validation\CanRefund( + $this->orderPaymentRepositoryMock, + $this->orderRepositoryMock + ); + } + + public function testValidateWrongInvoiceState() + { + $this->invoiceMock->expects($this->exactly(2)) + ->method('getState') + ->willReturnOnConsecutiveCalls( + \Magento\Sales\Model\Order\Invoice::STATE_OPEN, + \Magento\Sales\Model\Order\Invoice::STATE_CANCELED + ); + $this->assertEquals( + [__('We can\'t create creditmemo for the invoice.')], + $this->validator->validate($this->invoiceMock) + ); + $this->assertEquals( + [__('We can\'t create creditmemo for the invoice.')], + $this->validator->validate($this->invoiceMock) + ); + } + + public function testValidateInvoiceSumWasRefunded() + { + $this->invoiceMock->expects($this->once()) + ->method('getState') + ->willReturn(\Magento\Sales\Model\Order\Invoice::STATE_PAID); + $this->invoiceMock->expects($this->once()) + ->method('getBaseGrandTotal') + ->willReturn(1); + $this->invoiceMock->expects($this->once()) + ->method('getBaseTotalRefunded') + ->willReturn(1); + $this->assertEquals( + [__('We can\'t create creditmemo for the invoice.')], + $this->validator->validate($this->invoiceMock) + ); + } + + public function testValidate() + { + $this->invoiceMock->expects($this->once()) + ->method('getState') + ->willReturn(\Magento\Sales\Model\Order\Invoice::STATE_PAID); + $orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($orderMock); + $orderMock->expects($this->once()) + ->method('getPayment') + ->willReturn($this->paymentMock); + $methodInstanceMock = $this->getMockBuilder(\Magento\Payment\Model\MethodInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->paymentMock->expects($this->once()) + ->method('getMethodInstance') + ->willReturn($methodInstanceMock); + $methodInstanceMock->expects($this->atLeastOnce()) + ->method('canRefund') + ->willReturn(true); + $this->invoiceMock->expects($this->once()) + ->method('getBaseGrandTotal') + ->willReturn(1); + $this->invoiceMock->expects($this->once()) + ->method('getBaseTotalRefunded') + ->willReturn(0); + $this->assertEquals( + [], + $this->validator->validate($this->invoiceMock) + ); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Validation/CanRefundTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Validation/CanRefundTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0b4246d46944444041d92089ddc19a4d270df86f --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Validation/CanRefundTest.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Test\Unit\Model\Order\Validation; + +use Magento\Sales\Model\Order; + +/** + * Class CanRefundTest + */ +class CanRefundTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Sales\Model\Order\Validation\CanRefund|\PHPUnit_Framework_MockObject_MockObject + */ + private $model; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManager; + + /** + * @var \Magento\Sales\Api\Data\OrderInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderMock; + + /** + * @var \Magento\Framework\Pricing\PriceCurrencyInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $priceCurrencyMock; + + protected function setUp() + { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->orderMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getStatus', 'getItems']) + ->getMockForAbstractClass(); + + $this->priceCurrencyMock = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->priceCurrencyMock->expects($this->any()) + ->method('round') + ->willReturnArgument(0); + $this->model = new \Magento\Sales\Model\Order\Validation\CanRefund( + $this->priceCurrencyMock + ); + } + + /** + * @param string $state + * + * @dataProvider canCreditmemoWrongStateDataProvider + */ + public function testCanCreditmemoWrongState($state) + { + $this->orderMock->expects($this->any()) + ->method('getState') + ->willReturn($state); + $this->orderMock->expects($this->once()) + ->method('getStatus') + ->willReturn('status'); + $this->orderMock->expects($this->never()) + ->method('getTotalPaid') + ->willReturn(15); + $this->orderMock->expects($this->never()) + ->method('getTotalRefunded') + ->willReturn(14); + $this->assertEquals( + [__('A creditmemo can not be created when an order has a status of %1', 'status')], + $this->model->validate($this->orderMock) + ); + } + + /** + * Data provider for testCanCreditmemoWrongState + * @return array + */ + public function canCreditmemoWrongStateDataProvider() + { + return [ + [Order::STATE_PAYMENT_REVIEW], + [Order::STATE_HOLDED], + [Order::STATE_CANCELED], + [Order::STATE_CLOSED], + ]; + } + + public function testCanCreditmemoNoMoney() + { + $this->orderMock->expects($this->any()) + ->method('getState') + ->willReturn(Order::STATE_PROCESSING); + $this->orderMock->expects($this->once()) + ->method('getTotalPaid') + ->willReturn(15); + $this->orderMock->expects($this->once()) + ->method('getTotalRefunded') + ->willReturn(15); + $this->assertEquals( + [ + __('The order does not allow a creditmemo to be created.') + ], + $this->model->validate($this->orderMock) + ); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php b/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1c4aab6dd2feb1f2ac4d260bc44245633bc17467 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php @@ -0,0 +1,491 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Test\Unit\Model; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Sales\Api\CreditmemoRepositoryInterface; +use Magento\Sales\Api\Data\CreditmemoCommentCreationInterface; +use Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface; +use Magento\Sales\Api\Data\CreditmemoInterface; +use Magento\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\InvoiceRepositoryInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Config as OrderConfig; +use Magento\Sales\Model\Order\Creditmemo\CreditmemoValidatorInterface; +use Magento\Sales\Model\Order\Creditmemo\ItemCreationValidatorInterface; +use Magento\Sales\Api\Data\CreditmemoItemCreationInterface; +use Magento\Sales\Model\Order\CreditmemoDocumentFactory; +use Magento\Sales\Model\Order\Invoice\InvoiceValidatorInterface; +use Magento\Sales\Model\Order\OrderStateResolverInterface; +use Magento\Sales\Model\Order\OrderValidatorInterface; +use Magento\Sales\Model\Order\PaymentAdapterInterface; +use Magento\Sales\Model\Order\Creditmemo\NotifierInterface; +use Magento\Sales\Model\RefundInvoice; +use Psr\Log\LoggerInterface; + +/** + * Class RefundInvoiceTest + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) + */ +class RefundInvoiceTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + private $resourceConnectionMock; + + /** + * @var OrderRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderRepositoryMock; + + /** + * @var InvoiceRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $invoiceRepositoryMock; + + /** + * @var CreditmemoDocumentFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoDocumentFactoryMock; + + /** + * @var CreditmemoValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoValidatorMock; + + /** + * @var OrderValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderValidatorMock; + + /** + * @var InvoiceValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $invoiceValidatorMock; + + /** + * @var PaymentAdapterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentAdapterMock; + + /** + * @var OrderStateResolverInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderStateResolverMock; + + /** + * @var OrderConfig|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var Order\CreditmemoRepository|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoRepositoryMock; + + /** + * @var NotifierInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $notifierMock; + + /** + * @var RefundInvoice|\PHPUnit_Framework_MockObject_MockObject + */ + private $refundInvoice; + + /** + * @var CreditmemoCreationArgumentsInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoCommentCreationMock; + + /** + * @var CreditmemoCommentCreationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoCreationArgumentsMock; + + /** + * @var OrderInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderMock; + + /** + * @var OrderInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $invoiceMock; + + /** + * @var CreditmemoInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoMock; + + /** + * @var AdapterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $adapterInterface; + + /** + * @var CreditmemoItemCreationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoItemCreationMock; + + /** + * @var ItemCreationValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $itemCreationValidatorMock; + + /** + * @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $loggerMock; + + protected function setUp() + { + $this->resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->invoiceRepositoryMock = $this->getMockBuilder(InvoiceRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoDocumentFactoryMock = $this->getMockBuilder(CreditmemoDocumentFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoValidatorMock = $this->getMockBuilder(CreditmemoValidatorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderValidatorMock = $this->getMockBuilder(OrderValidatorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->invoiceValidatorMock = $this->getMockBuilder(InvoiceValidatorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->paymentAdapterMock = $this->getMockBuilder(PaymentAdapterInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderStateResolverMock = $this->getMockBuilder(OrderStateResolverInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->configMock = $this->getMockBuilder(OrderConfig::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->creditmemoRepositoryMock = $this->getMockBuilder(CreditmemoRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->notifierMock = $this->getMockBuilder(NotifierInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->creditmemoCommentCreationMock = $this->getMockBuilder(CreditmemoCommentCreationInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->creditmemoCreationArgumentsMock = $this->getMockBuilder(CreditmemoCreationArgumentsInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->invoiceMock = $this->getMockBuilder(InvoiceInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->adapterInterface = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->creditmemoItemCreationMock = $this->getMockBuilder(CreditmemoItemCreationInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->itemCreationValidatorMock = $this->getMockBuilder(ItemCreationValidatorInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->refundInvoice = new RefundInvoice( + $this->resourceConnectionMock, + $this->orderStateResolverMock, + $this->orderRepositoryMock, + $this->invoiceRepositoryMock, + $this->orderValidatorMock, + $this->invoiceValidatorMock, + $this->creditmemoValidatorMock, + $this->itemCreationValidatorMock, + $this->creditmemoRepositoryMock, + $this->paymentAdapterMock, + $this->creditmemoDocumentFactoryMock, + $this->notifierMock, + $this->configMock, + $this->loggerMock + ); + } + + /** + * @dataProvider dataProvider + */ + public function testOrderCreditmemo($invoiceId, $items, $notify, $appendComment) + { + $this->resourceConnectionMock->expects($this->once()) + ->method('getConnection') + ->with('sales') + ->willReturn($this->adapterInterface); + + $this->invoiceRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->invoiceMock); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + + $this->creditmemoDocumentFactoryMock->expects($this->once()) + ->method('createFromInvoice') + ->with( + $this->invoiceMock, + $items, + $this->creditmemoCommentCreationMock, + ($appendComment && $notify), + $this->creditmemoCreationArgumentsMock + )->willReturn($this->creditmemoMock); + + $this->creditmemoValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->creditmemoMock) + ->willReturn([]); + $this->orderValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->orderMock) + ->willReturn([]); + $this->invoiceValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->invoiceMock) + ->willReturn([]); + $this->itemCreationValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->creditmemoItemCreationMock) + ->willReturn([]); + $this->paymentAdapterMock->expects($this->once()) + ->method('refund') + ->with($this->creditmemoMock, $this->orderMock) + ->willReturn($this->orderMock); + $this->orderStateResolverMock->expects($this->once()) + ->method('getStateForOrder') + ->with($this->orderMock, []) + ->willReturn(Order::STATE_CLOSED); + $this->orderMock->expects($this->once()) + ->method('setState') + ->with(Order::STATE_CLOSED) + ->willReturnSelf(); + $this->orderMock->expects($this->once()) + ->method('getState') + ->willReturn(Order::STATE_CLOSED); + $this->configMock->expects($this->once()) + ->method('getStateDefaultStatus') + ->with(Order::STATE_CLOSED) + ->willReturn('Closed'); + $this->orderMock->expects($this->once()) + ->method('setStatus') + ->with('Closed') + ->willReturnSelf(); + $this->creditmemoMock->expects($this->once()) + ->method('setState') + ->with(\Magento\Sales\Model\Order\Creditmemo::STATE_REFUNDED) + ->willReturnSelf(); + + $this->creditmemoRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->creditmemoMock) + ->willReturn($this->creditmemoMock); + $this->orderRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->orderMock) + ->willReturn($this->orderMock); + if ($notify) { + $this->notifierMock->expects($this->once()) + ->method('notify') + ->with($this->orderMock, $this->creditmemoMock, $this->creditmemoCommentCreationMock); + } + $this->creditmemoMock->expects($this->once()) + ->method('getEntityId') + ->willReturn(2); + + $this->assertEquals( + 2, + $this->refundInvoice->execute( + $invoiceId, + $items, + false, + $notify, + $appendComment, + $this->creditmemoCommentCreationMock, + $this->creditmemoCreationArgumentsMock + ) + ); + } + + /** + * @expectedException \Magento\Sales\Api\Exception\DocumentValidationExceptionInterface + */ + public function testDocumentValidationException() + { + $invoiceId = 1; + $items = [1 => $this->creditmemoItemCreationMock]; + $notify = true; + $appendComment = true; + $errorMessages = ['error1', 'error2']; + + $this->invoiceRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->invoiceMock); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + + $this->creditmemoDocumentFactoryMock->expects($this->once()) + ->method('createFromInvoice') + ->with( + $this->invoiceMock, + $items, + $this->creditmemoCommentCreationMock, + ($appendComment && $notify), + $this->creditmemoCreationArgumentsMock + )->willReturn($this->creditmemoMock); + + $this->creditmemoValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->creditmemoMock) + ->willReturn($errorMessages); + $this->orderValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->orderMock) + ->willReturn([]); + $this->invoiceValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->invoiceMock) + ->willReturn([]); + $this->itemCreationValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->creditmemoItemCreationMock) + ->willReturn([]); + + $this->assertEquals( + $errorMessages, + $this->refundInvoice->execute( + $invoiceId, + $items, + false, + $notify, + $appendComment, + $this->creditmemoCommentCreationMock, + $this->creditmemoCreationArgumentsMock + ) + ); + } + + /** + * @expectedException \Magento\Sales\Api\Exception\CouldNotRefundExceptionInterface + */ + public function testCouldNotCreditmemoException() + { + $invoiceId = 1; + $items = [1 => $this->creditmemoItemCreationMock]; + $notify = true; + $appendComment = true; + $this->resourceConnectionMock->expects($this->once()) + ->method('getConnection') + ->with('sales') + ->willReturn($this->adapterInterface); + + $this->invoiceRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->invoiceMock); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + + $this->creditmemoDocumentFactoryMock->expects($this->once()) + ->method('createFromInvoice') + ->with( + $this->invoiceMock, + $items, + $this->creditmemoCommentCreationMock, + ($appendComment && $notify), + $this->creditmemoCreationArgumentsMock + )->willReturn($this->creditmemoMock); + + $this->creditmemoValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->creditmemoMock) + ->willReturn([]); + $this->orderValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->orderMock) + ->willReturn([]); + $this->invoiceValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->invoiceMock) + ->willReturn([]); + $this->itemCreationValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->creditmemoItemCreationMock) + ->willReturn([]); + $e = new \Exception(); + + $this->paymentAdapterMock->expects($this->once()) + ->method('refund') + ->with($this->creditmemoMock, $this->orderMock) + ->willThrowException($e); + + $this->loggerMock->expects($this->once()) + ->method('critical') + ->with($e); + + $this->adapterInterface->expects($this->once()) + ->method('rollBack'); + + $this->refundInvoice->execute( + $invoiceId, + $items, + false, + $notify, + $appendComment, + $this->creditmemoCommentCreationMock, + $this->creditmemoCreationArgumentsMock + ); + } + + public function dataProvider() + { + $creditmemoItemCreationMock = $this->getMockBuilder(CreditmemoItemCreationInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + return [ + 'TestWithNotifyTrue' => [1, [1 => $creditmemoItemCreationMock], true, true], + 'TestWithNotifyFalse' => [1, [1 => $creditmemoItemCreationMock], false, true], + ]; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..7d684695664ba1d1b63a5b78cf610da019cf39bb --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php @@ -0,0 +1,423 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Test\Unit\Model; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Sales\Api\CreditmemoRepositoryInterface; +use Magento\Sales\Api\Data\CreditmemoCommentCreationInterface; +use Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface; +use Magento\Sales\Api\Data\CreditmemoInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Config as OrderConfig; +use Magento\Sales\Model\Order\Creditmemo\CreditmemoValidatorInterface; +use Magento\Sales\Model\Order\Creditmemo\Item\Validation\CreationQuantityValidator; +use Magento\Sales\Model\Order\CreditmemoDocumentFactory; +use Magento\Sales\Model\Order\OrderStateResolverInterface; +use Magento\Sales\Model\Order\OrderValidatorInterface; +use Magento\Sales\Model\Order\PaymentAdapterInterface; +use Magento\Sales\Model\Order\Creditmemo\NotifierInterface; +use Magento\Sales\Model\RefundOrder; +use Psr\Log\LoggerInterface; +use Magento\Sales\Api\Data\CreditmemoItemCreationInterface; + +/** + * Class RefundOrderTest + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) + */ +class RefundOrderTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + private $resourceConnectionMock; + + /** + * @var OrderRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderRepositoryMock; + + /** + * @var CreditmemoDocumentFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoDocumentFactoryMock; + + /** + * @var CreditmemoValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoValidatorMock; + + /** + * @var OrderValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderValidatorMock; + + /** + * @var PaymentAdapterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentAdapterMock; + + /** + * @var OrderStateResolverInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderStateResolverMock; + + /** + * @var OrderConfig|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var Order\CreditmemoRepository|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoRepositoryMock; + + /** + * @var NotifierInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $notifierMock; + + /** + * @var RefundOrder|\PHPUnit_Framework_MockObject_MockObject + */ + private $refundOrder; + + /** + * @var CreditmemoCreationArgumentsInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoCommentCreationMock; + + /** + * @var CreditmemoCommentCreationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoCreationArgumentsMock; + + /** + * @var OrderInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderMock; + + /** + * @var CreditmemoInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoMock; + + /** + * @var AdapterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $adapterInterface; + + /** + * @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $loggerMock; + + /** + * @var Order\Creditmemo\ItemCreationValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $itemCreationValidatorMock; + + /** + * @var CreditmemoItemCreationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoItemCreationMock; + + protected function setUp() + { + $this->resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoDocumentFactoryMock = $this->getMockBuilder(CreditmemoDocumentFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoValidatorMock = $this->getMockBuilder(CreditmemoValidatorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderValidatorMock = $this->getMockBuilder(OrderValidatorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->paymentAdapterMock = $this->getMockBuilder(PaymentAdapterInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderStateResolverMock = $this->getMockBuilder(OrderStateResolverInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->configMock = $this->getMockBuilder(OrderConfig::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoRepositoryMock = $this->getMockBuilder(CreditmemoRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->notifierMock = $this->getMockBuilder(NotifierInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->creditmemoCommentCreationMock = $this->getMockBuilder(CreditmemoCommentCreationInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->creditmemoCreationArgumentsMock = $this->getMockBuilder(CreditmemoCreationArgumentsInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->adapterInterface = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->itemCreationValidatorMock = $this->getMockBuilder(Order\Creditmemo\ItemCreationValidatorInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->creditmemoItemCreationMock = $this->getMockBuilder(CreditmemoItemCreationInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->refundOrder = new RefundOrder( + $this->resourceConnectionMock, + $this->orderStateResolverMock, + $this->orderRepositoryMock, + $this->orderValidatorMock, + $this->creditmemoValidatorMock, + $this->itemCreationValidatorMock, + $this->creditmemoRepositoryMock, + $this->paymentAdapterMock, + $this->creditmemoDocumentFactoryMock, + $this->notifierMock, + $this->configMock, + $this->loggerMock + ); + } + + /** + * @dataProvider dataProvider + */ + public function testOrderCreditmemo($orderId, $notify, $appendComment) + { + $items = [$this->creditmemoItemCreationMock]; + $this->resourceConnectionMock->expects($this->once()) + ->method('getConnection') + ->with('sales') + ->willReturn($this->adapterInterface); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + $this->creditmemoDocumentFactoryMock->expects($this->once()) + ->method('createFromOrder') + ->with( + $this->orderMock, + $items, + $this->creditmemoCommentCreationMock, + ($appendComment && $notify), + $this->creditmemoCreationArgumentsMock + )->willReturn($this->creditmemoMock); + $this->creditmemoValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->creditmemoMock) + ->willReturn([]); + $this->orderValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->orderMock) + ->willReturn([]); + $this->itemCreationValidatorMock->expects($this->once()) + ->method('validate') + ->with( + reset($items), + [CreationQuantityValidator::class], + $this->orderMock + )->willReturn([]); + $this->paymentAdapterMock->expects($this->once()) + ->method('refund') + ->with($this->creditmemoMock, $this->orderMock) + ->willReturn($this->orderMock); + $this->orderStateResolverMock->expects($this->once()) + ->method('getStateForOrder') + ->with($this->orderMock, []) + ->willReturn(Order::STATE_CLOSED); + + $this->orderMock->expects($this->once()) + ->method('setState') + ->with(Order::STATE_CLOSED) + ->willReturnSelf(); + + $this->orderMock->expects($this->once()) + ->method('getState') + ->willReturn(Order::STATE_CLOSED); + + $this->configMock->expects($this->once()) + ->method('getStateDefaultStatus') + ->with(Order::STATE_CLOSED) + ->willReturn('Closed'); + + $this->orderMock->expects($this->once()) + ->method('setStatus') + ->with('Closed') + ->willReturnSelf(); + + $this->creditmemoMock->expects($this->once()) + ->method('setState') + ->with(\Magento\Sales\Model\Order\Creditmemo::STATE_REFUNDED) + ->willReturnSelf(); + + $this->creditmemoRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->creditmemoMock) + ->willReturn($this->creditmemoMock); + + $this->orderRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->orderMock) + ->willReturn($this->orderMock); + + if ($notify) { + $this->notifierMock->expects($this->once()) + ->method('notify') + ->with($this->orderMock, $this->creditmemoMock, $this->creditmemoCommentCreationMock); + } + + $this->creditmemoMock->expects($this->once()) + ->method('getEntityId') + ->willReturn(2); + + $this->assertEquals( + 2, + $this->refundOrder->execute( + $orderId, + $items, + $notify, + $appendComment, + $this->creditmemoCommentCreationMock, + $this->creditmemoCreationArgumentsMock + ) + ); + } + + /** + * @expectedException \Magento\Sales\Api\Exception\DocumentValidationExceptionInterface + */ + public function testDocumentValidationException() + { + $orderId = 1; + $items = [$this->creditmemoItemCreationMock]; + $notify = true; + $appendComment = true; + $errorMessages = ['error1', 'error2']; + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + + $this->creditmemoDocumentFactoryMock->expects($this->once()) + ->method('createFromOrder') + ->with( + $this->orderMock, + $items, + $this->creditmemoCommentCreationMock, + ($appendComment && $notify), + $this->creditmemoCreationArgumentsMock + )->willReturn($this->creditmemoMock); + + $this->creditmemoValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->creditmemoMock) + ->willReturn($errorMessages); + $this->orderValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->orderMock) + ->willReturn([]); + $this->itemCreationValidatorMock->expects($this->once()) + ->method('validate') + ->with(reset($items), [CreationQuantityValidator::class], $this->orderMock) + ->willReturn([]); + + $this->assertEquals( + $errorMessages, + $this->refundOrder->execute( + $orderId, + $items, + $notify, + $appendComment, + $this->creditmemoCommentCreationMock, + $this->creditmemoCreationArgumentsMock + ) + ); + } + + /** + * @expectedException \Magento\Sales\Api\Exception\CouldNotRefundExceptionInterface + */ + public function testCouldNotCreditmemoException() + { + $orderId = 1; + $items = [$this->creditmemoItemCreationMock]; + $notify = true; + $appendComment = true; + $this->resourceConnectionMock->expects($this->once()) + ->method('getConnection') + ->with('sales') + ->willReturn($this->adapterInterface); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + $this->creditmemoDocumentFactoryMock->expects($this->once()) + ->method('createFromOrder') + ->with( + $this->orderMock, + $items, + $this->creditmemoCommentCreationMock, + ($appendComment && $notify), + $this->creditmemoCreationArgumentsMock + )->willReturn($this->creditmemoMock); + $this->itemCreationValidatorMock->expects($this->once()) + ->method('validate') + ->with(reset($items), [CreationQuantityValidator::class], $this->orderMock) + ->willReturn([]); + $this->creditmemoValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->creditmemoMock) + ->willReturn([]); + $this->orderValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->orderMock) + ->willReturn([]); + $e = new \Exception(); + $this->paymentAdapterMock->expects($this->once()) + ->method('refund') + ->with($this->creditmemoMock, $this->orderMock) + ->willThrowException($e); + $this->loggerMock->expects($this->once()) + ->method('critical') + ->with($e); + $this->adapterInterface->expects($this->once()) + ->method('rollBack'); + + $this->refundOrder->execute( + $orderId, + $items, + $notify, + $appendComment, + $this->creditmemoCommentCreationMock, + $this->creditmemoCreationArgumentsMock + ); + } + + public function dataProvider() + { + return [ + 'TestWithNotifyTrue' => [1, true, true], + 'TestWithNotifyFalse' => [1, false, true], + ]; + } +} diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml index dfdb0f6a261c51490e99955bdd61fd1bae328e60..03437a8e9801324e9346f55a63ffb6c7d5582a31 100644 --- a/app/code/Magento/Sales/etc/di.xml +++ b/app/code/Magento/Sales/etc/di.xml @@ -47,6 +47,8 @@ <preference for="Magento\Sales\Api\CreditmemoItemRepositoryInterface" type="Magento\Sales\Api\Data\CreditmemoItem\Repository"/> <preference for="Magento\Sales\Api\CreditmemoRepositoryInterface" type="Magento\Sales\Model\Order\CreditmemoRepository"/> <preference for="Magento\Sales\Api\CreditmemoManagementInterface" type="Magento\Sales\Model\Service\CreditmemoService"/> + <preference for="Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface" type="Magento\Sales\Model\Order\Creditmemo\CreationArguments"/> + <preference for="Magento\Sales\Api\Data\CreditmemoItemCreationInterface" type="Magento\Sales\Model\Order\Creditmemo\ItemCreation"/> <preference for="Magento\Sales\Api\InvoiceCommentRepositoryInterface" type="Magento\Sales\Api\Data\InvoiceComment\Repository"/> <preference for="Magento\Sales\Api\InvoiceItemRepositoryInterface" type="Magento\Sales\Api\Data\InvoiceItem\Repository"/> <preference for="Magento\Sales\Api\InvoiceRepositoryInterface" type="Magento\Sales\Model\Order\InvoiceRepository"/> @@ -55,6 +57,7 @@ <preference for="Magento\Sales\Api\Data\InvoiceItemCreationInterface" type="Magento\Sales\Model\Order\Invoice\ItemCreation"/> <preference for="Magento\Sales\Api\Data\InvoiceCommentCreationInterface" type="Magento\Sales\Model\Order\Invoice\CommentCreation"/> <preference for="Magento\Sales\Api\Data\ShipmentCommentCreationInterface" type="Magento\Sales\Model\Order\Shipment\CommentCreation"/> + <preference for="Magento\Sales\Api\Data\CreditmemoCommentCreationInterface" type="Magento\Sales\Model\Order\Creditmemo\CommentCreation"/> <preference for="Magento\Sales\Api\OrderAddressRepositoryInterface" type="Magento\Sales\Model\Order\AddressRepository"/> <preference for="Magento\Sales\Api\OrderCustomerManagementInterface" type="Magento\Sales\Model\Order\CustomerManagement"/> <preference for="Magento\Sales\Api\OrderItemRepositoryInterface" type="Magento\Sales\Model\Order\ItemRepository"/> @@ -100,6 +103,11 @@ <preference for="Magento\Sales\Model\Order\OrderValidatorInterface" type="Magento\Sales\Model\Order\OrderValidator"/> <preference for="Magento\Sales\Model\Order\Invoice\InvoiceValidatorInterface" type="Magento\Sales\Model\Order\Invoice\InvoiceValidator"/> <preference for="Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface" type="Magento\Sales\Model\Order\Shipment\ShipmentValidator"/> + <preference for="Magento\Sales\Model\Order\Creditmemo\CreditmemoValidatorInterface" type="Magento\Sales\Model\Order\Creditmemo\CreditmemoValidator"/> + <preference for="Magento\Sales\Model\Order\Creditmemo\ItemCreationValidatorInterface" type="Magento\Sales\Model\Order\Creditmemo\ItemCreationValidator"/> + <preference for="Magento\Sales\Model\Order\Creditmemo\NotifierInterface" type="Magento\Sales\Model\Order\Creditmemo\Notifier"/> + <preference for="Magento\Sales\Api\RefundOrderInterface" type="Magento\Sales\Model\RefundOrder"/> + <preference for="Magento\Sales\Api\RefundInvoiceInterface" type="Magento\Sales\Model\RefundInvoice"/> <type name="Magento\Sales\Model\ResourceModel\Report" shared="false"/> <type name="Magento\Sales\Model\Order\Pdf\Config\Reader"> <arguments> @@ -926,6 +934,13 @@ </argument> </arguments> </type> + <type name="Magento\Sales\Model\Order\Creditmemo\Notifier"> + <arguments> + <argument name="senders" xsi:type="array"> + <item name="email" xsi:type="object">Magento\Sales\Model\Order\Creditmemo\Sender\EmailSender</item> + </argument> + </arguments> + </type> <type name="Magento\Framework\EntityManager\HydratorPool"> <arguments> <argument name="hydrators" xsi:type="array"> diff --git a/app/code/Magento/Sales/etc/webapi.xml b/app/code/Magento/Sales/etc/webapi.xml index 4c7fe03a201f8b25c2af83219fd0576da4c2cd3e..b2a88b8cad709d1b0726a66d70b9779241840409 100644 --- a/app/code/Magento/Sales/etc/webapi.xml +++ b/app/code/Magento/Sales/etc/webapi.xml @@ -133,6 +133,12 @@ <resource ref="Magento_Sales::sales" /> </resources> </route> + <route url="/V1/invoice/:invoiceId/refund" method="POST"> + <service class="Magento\Sales\Api\RefundInvoiceInterface" method="execute"/> + <resources> + <resource ref="Magento_Sales::sales" /> + </resources> + </route> <route url="/V1/creditmemo/:id/comments" method="GET"> <service class="Magento\Sales\Api\CreditmemoManagementInterface" method="getCommentsList"/> <resources> @@ -181,6 +187,12 @@ <resource ref="Magento_Sales::sales" /> </resources> </route> + <route url="/V1/order/:orderId/refund" method="POST"> + <service class="Magento\Sales\Api\RefundOrderInterface" method="execute"/> + <resources> + <resource ref="Magento_Sales::sales" /> + </resources> + </route> <route url="/V1/shipment/:id" method="GET"> <service class="Magento\Sales\Api\ShipmentRepositoryInterface" method="get"/> <resources> diff --git a/app/code/Magento/Theme/Model/Theme/ThemeProvider.php b/app/code/Magento/Theme/Model/Theme/ThemeProvider.php index 29da9c3220ee29fa60b24085336eaff919d5b3dd..9fd3ce94dac45888687cbe6bff26f86a2ac9d2e7 100644 --- a/app/code/Magento/Theme/Model/Theme/ThemeProvider.php +++ b/app/code/Magento/Theme/Model/Theme/ThemeProvider.php @@ -22,6 +22,11 @@ class ThemeProvider implements \Magento\Framework\View\Design\Theme\ThemeProvide */ protected $cache; + /** + * @var \Magento\Framework\View\Design\ThemeInterface[] + */ + private $themes; + /** * ThemeProvider constructor. * @@ -44,10 +49,14 @@ class ThemeProvider implements \Magento\Framework\View\Design\Theme\ThemeProvide */ public function getThemeByFullPath($fullPath) { + if (isset($this->themes[$fullPath])) { + return $this->themes[$fullPath]; + } /** @var $themeCollection \Magento\Theme\Model\ResourceModel\Theme\Collection */ $theme = $this->cache->load('theme'. $fullPath); if ($theme) { - return unserialize($theme); + $this->themes[$fullPath] = unserialize($theme); + return $this->themes[$fullPath]; } $themeCollection = $this->collectionFactory->create(); $item = $themeCollection->getThemeByFullPath($fullPath); @@ -55,7 +64,9 @@ class ThemeProvider implements \Magento\Framework\View\Design\Theme\ThemeProvide $themeData = serialize($item); $this->cache->save($themeData, 'theme' . $fullPath); $this->cache->save($themeData, 'theme-by-id-' . $item->getId()); + $this->themes[$fullPath] = $item; } + return $item; } @@ -77,15 +88,20 @@ class ThemeProvider implements \Magento\Framework\View\Design\Theme\ThemeProvide */ public function getThemeById($themeId) { + if (isset($this->themes[$themeId])) { + return $this->themes[$themeId]; + } $theme = $this->cache->load('theme-by-id-' . $themeId); if ($theme) { - return unserialize($theme); + $this->themes[$themeId] = unserialize($theme); + return $this->themes[$themeId]; } /** @var $themeModel \Magento\Framework\View\Design\ThemeInterface */ $themeModel = $this->themeFactory->create(); $themeModel->load($themeId); if ($themeModel->getId()) { $this->cache->save(serialize($themeModel), 'theme-by-id-' . $themeId); + $this->themes[$themeId] = $themeModel; } return $themeModel; } diff --git a/app/code/Magento/Vault/Model/Method/Vault.php b/app/code/Magento/Vault/Model/Method/Vault.php index 0e6689461e14b6d8647693aa2dc942a240bc2cc2..ce85936607192b81fcdd9da59fb48704fc92d48e 100644 --- a/app/code/Magento/Vault/Model/Method/Vault.php +++ b/app/code/Magento/Vault/Model/Method/Vault.php @@ -579,7 +579,7 @@ final class Vault implements VaultPaymentInterface public function isAvailable(\Magento\Quote\Api\Data\CartInterface $quote = null) { return $this->getVaultProvider()->isAvailable($quote) - && $this->config->getValue(self::$activeKey, $this->getStore() ?: $quote->getStoreId()); + && $this->config->getValue(self::$activeKey, $this->getStore() ?: ($quote ? $quote->getStoreId() : null)); } /** diff --git a/app/code/Magento/Vault/Model/Ui/TokensConfigProvider.php b/app/code/Magento/Vault/Model/Ui/TokensConfigProvider.php index 434a5875b5bb1b4bffb1b77547e64740107b902d..6a86ed694642c31407ba0c08e3ac39f646244cca 100644 --- a/app/code/Magento/Vault/Model/Ui/TokensConfigProvider.php +++ b/app/code/Magento/Vault/Model/Ui/TokensConfigProvider.php @@ -7,7 +7,7 @@ namespace Magento\Vault\Model\Ui; use Magento\Checkout\Model\ConfigProviderInterface; use Magento\Framework\App\ObjectManager; -use Magento\Payment\Helper\Data; +use Magento\Payment\Api\Data\PaymentMethodInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Vault\Model\CustomerTokenManagement; use Magento\Vault\Model\VaultPaymentInterface; @@ -39,9 +39,14 @@ final class TokensConfigProvider implements ConfigProviderInterface private $customerTokenManagement; /** - * @var Data + * @var \Magento\Payment\Api\PaymentMethodListInterface */ - private $paymentDataHelper; + private $paymentMethodList; + + /** + * @var \Magento\Payment\Model\Method\InstanceFactory + */ + private $paymentMethodInstanceFactory; /** * Constructor @@ -98,21 +103,16 @@ final class TokensConfigProvider implements ConfigProviderInterface } /** - * Get list of available vault ui token providers + * Get list of available vault ui token providers. + * * @return TokenUiComponentProviderInterface[] */ private function getComponentProviders() { $providers = []; - $storeId = $this->storeManager->getStore()->getId(); - $paymentMethods = $this->getPaymentDataHelper()->getStoreMethods($storeId); + $vaultPaymentMethods = $this->getVaultPaymentMethodList(); - foreach ($paymentMethods as $method) { - /** VaultPaymentInterface $method */ - if (!$method instanceof VaultPaymentInterface || !$method->isActive($storeId)) { - continue; - } - + foreach ($vaultPaymentMethods as $method) { $providerCode = $method->getProviderCode(); $componentProvider = $this->getComponentProvider($providerCode); if ($componentProvider === null) { @@ -139,15 +139,60 @@ final class TokensConfigProvider implements ConfigProviderInterface } /** - * Get payment data helper instance - * @return Data + * Get list of active Vault payment methods. + * + * @return VaultPaymentInterface[] + */ + private function getVaultPaymentMethodList() + { + $storeId = $this->storeManager->getStore()->getId(); + + $paymentMethods = array_map( + function (PaymentMethodInterface $paymentMethod) { + return $this->getPaymentMethodInstanceFactory()->create($paymentMethod); + }, + $this->getPaymentMethodList()->getActiveList($storeId) + ); + + $availableMethods = array_filter( + $paymentMethods, + function (\Magento\Payment\Model\MethodInterface $methodInstance) { + return $methodInstance instanceof VaultPaymentInterface; + } + ); + + return $availableMethods; + } + + /** + * Get payment method list. + * + * @return \Magento\Payment\Api\PaymentMethodListInterface + * @deprecated + */ + private function getPaymentMethodList() + { + if ($this->paymentMethodList === null) { + $this->paymentMethodList = ObjectManager::getInstance()->get( + \Magento\Payment\Api\PaymentMethodListInterface::class + ); + } + return $this->paymentMethodList; + } + + /** + * Get payment method instance factory. + * + * @return \Magento\Payment\Model\Method\InstanceFactory * @deprecated */ - private function getPaymentDataHelper() + private function getPaymentMethodInstanceFactory() { - if ($this->paymentDataHelper === null) { - $this->paymentDataHelper = ObjectManager::getInstance()->get(Data::class); + if ($this->paymentMethodInstanceFactory === null) { + $this->paymentMethodInstanceFactory = ObjectManager::getInstance()->get( + \Magento\Payment\Model\Method\InstanceFactory::class + ); } - return $this->paymentDataHelper; + return $this->paymentMethodInstanceFactory; } } diff --git a/app/code/Magento/Vault/Model/Ui/VaultConfigProvider.php b/app/code/Magento/Vault/Model/Ui/VaultConfigProvider.php index 8bf88e7a94f509ddd9d23c239dd59dccb15d4e85..9cd7b97562df98e309b9ddd182a5ced144b2e555 100644 --- a/app/code/Magento/Vault/Model/Ui/VaultConfigProvider.php +++ b/app/code/Magento/Vault/Model/Ui/VaultConfigProvider.php @@ -8,7 +8,7 @@ namespace Magento\Vault\Model\Ui; use Magento\Checkout\Model\ConfigProviderInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\Session\SessionManagerInterface; -use Magento\Payment\Helper\Data; +use Magento\Payment\Api\Data\PaymentMethodInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Vault\Model\VaultPaymentInterface; @@ -32,9 +32,14 @@ class VaultConfigProvider implements ConfigProviderInterface private $session; /** - * @var Data + * @var \Magento\Payment\Api\PaymentMethodListInterface */ - private $paymentDataHelper; + private $paymentMethodList; + + /** + * @var \Magento\Payment\Model\Method\InstanceFactory + */ + private $paymentMethodInstanceFactory; /** * VaultConfigProvider constructor. @@ -73,36 +78,60 @@ class VaultConfigProvider implements ConfigProviderInterface } /** - * Get list of active Vault payment methods - * @return array + * Get list of active Vault payment methods. + * + * @return VaultPaymentInterface[] */ private function getVaultPaymentMethodList() { - $availableMethods = []; $storeId = $this->storeManager->getStore()->getId(); - $paymentMethods = $this->getPaymentDataHelper()->getStoreMethods($storeId); - foreach ($paymentMethods as $method) { - /** VaultPaymentInterface $method */ - if (!$method instanceof VaultPaymentInterface) { - continue; + $paymentMethods = array_map( + function (PaymentMethodInterface $paymentMethod) { + return $this->getPaymentMethodInstanceFactory()->create($paymentMethod); + }, + $this->getPaymentMethodList()->getActiveList($storeId) + ); + + $availableMethods = array_filter( + $paymentMethods, + function (\Magento\Payment\Model\MethodInterface $methodInstance) { + return $methodInstance instanceof VaultPaymentInterface; } - $availableMethods[] = $method; - } + ); return $availableMethods; } /** - * Get payment data helper instance - * @return Data + * Get payment method list. + * + * @return \Magento\Payment\Api\PaymentMethodListInterface + * @deprecated + */ + private function getPaymentMethodList() + { + if ($this->paymentMethodList === null) { + $this->paymentMethodList = ObjectManager::getInstance()->get( + \Magento\Payment\Api\PaymentMethodListInterface::class + ); + } + return $this->paymentMethodList; + } + + /** + * Get payment method instance factory. + * + * @return \Magento\Payment\Model\Method\InstanceFactory * @deprecated */ - private function getPaymentDataHelper() + private function getPaymentMethodInstanceFactory() { - if ($this->paymentDataHelper === null) { - $this->paymentDataHelper = ObjectManager::getInstance()->get(Data::class); + if ($this->paymentMethodInstanceFactory === null) { + $this->paymentMethodInstanceFactory = ObjectManager::getInstance()->get( + \Magento\Payment\Model\Method\InstanceFactory::class + ); } - return $this->paymentDataHelper; + return $this->paymentMethodInstanceFactory; } } diff --git a/app/code/Magento/Vault/Test/Unit/Model/Method/VaultTest.php b/app/code/Magento/Vault/Test/Unit/Model/Method/VaultTest.php index fc6f3e4d2390b58303ff9261b86787fa50b1535a..e5c83911025246d10c56687604d343b233db382b 100644 --- a/app/code/Magento/Vault/Test/Unit/Model/Method/VaultTest.php +++ b/app/code/Magento/Vault/Test/Unit/Model/Method/VaultTest.php @@ -274,4 +274,32 @@ class VaultTest extends \PHPUnit_Framework_TestCase ['isAvailableProvider' => true, 'isActiveVault' => true, 'expected' => true], ]; } + + /** + * @covers \Magento\Vault\Model\Method\Vault::isAvailable + */ + public function testIsAvailableWithoutQuote() + { + $quote = null; + + $vaultProvider = $this->getMockForAbstractClass(MethodInterface::class); + $config = $this->getMockForAbstractClass(ConfigInterface::class); + + $vaultProvider->expects(static::once()) + ->method('isAvailable') + ->with($quote) + ->willReturn(true); + + $config->expects(static::once()) + ->method('getValue') + ->with('active', $quote) + ->willReturn(false); + + /** @var Vault $model */ + $model = $this->objectManager->getObject(Vault::class, [ + 'config' => $config, + 'vaultProvider' => $vaultProvider + ]); + static::assertFalse($model->isAvailable($quote)); + } } diff --git a/app/code/Magento/Vault/Test/Unit/Model/Ui/TokensConfigProviderTest.php b/app/code/Magento/Vault/Test/Unit/Model/Ui/TokensConfigProviderTest.php index e64fff27ff14ca0ad0aa0ffc6d4c0eda539f8be1..3e4b8b145af67da759d6445234d18bd528832810 100644 --- a/app/code/Magento/Vault/Test/Unit/Model/Ui/TokensConfigProviderTest.php +++ b/app/code/Magento/Vault/Test/Unit/Model/Ui/TokensConfigProviderTest.php @@ -7,7 +7,6 @@ namespace Magento\Vault\Test\Unit\Model\Ui; use Magento\Customer\Model\Session; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Payment\Helper\Data; use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Vault\Api\Data\PaymentTokenInterface; @@ -33,10 +32,25 @@ class TokensConfigProviderTest extends \PHPUnit_Framework_TestCase private $storeManager; /** - * @var VaultPaymentInterface|MockObject + * @var \Magento\Payment\Api\PaymentMethodListInterface|MockObject + */ + private $paymentMethodList; + + /** + * @var \Magento\Payment\Model\Method\InstanceFactory|MockObject + */ + private $paymentMethodInstanceFactory; + + /** + * @var \Magento\Payment\Api\Data\PaymentMethodInterface|MockObject */ private $vaultPayment; + /** + * @var VaultPaymentInterface|MockObject + */ + private $vaultPaymentInstance; + /** * @var StoreInterface|MockObject */ @@ -47,11 +61,6 @@ class TokensConfigProviderTest extends \PHPUnit_Framework_TestCase */ private $customerTokenManagement; - /** - * @var Data|MockObject - */ - private $paymentDataHelper; - /** * @var ObjectManager */ @@ -59,13 +68,18 @@ class TokensConfigProviderTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->vaultPayment = $this->getMock(VaultPaymentInterface::class); + $this->paymentMethodList = $this->getMockBuilder(\Magento\Payment\Api\PaymentMethodListInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->paymentMethodInstanceFactory = $this->getMockBuilder( + \Magento\Payment\Model\Method\InstanceFactory::class + )->disableOriginalConstructor()->getMock(); + + $this->vaultPayment = $this->getMockForAbstractClass(\Magento\Payment\Api\Data\PaymentMethodInterface::class); + $this->vaultPaymentInstance = $this->getMockForAbstractClass(VaultPaymentInterface::class); $this->storeManager = $this->getMock(StoreManagerInterface::class); $this->store = $this->getMock(StoreInterface::class); - $this->paymentDataHelper = $this->getMockBuilder(Data::class) - ->disableOriginalConstructor() - ->setMethods(['getStoreMethods']) - ->getMock(); $this->objectManager = new ObjectManager($this); $this->customerTokenManagement = $this->getMockBuilder(CustomerTokenManagement::class) @@ -99,17 +113,17 @@ class TokensConfigProviderTest extends \PHPUnit_Framework_TestCase $this->store->expects(static::once()) ->method('getId') ->willReturn($storeId); - - $this->paymentDataHelper->expects(static::once()) - ->method('getStoreMethods') + + $this->paymentMethodList->expects(static::once()) + ->method('getActiveList') ->with($storeId) ->willReturn([$this->vaultPayment]); + + $this->paymentMethodInstanceFactory->expects($this->once()) + ->method('create') + ->willReturn($this->vaultPaymentInstance); - $this->vaultPayment->expects(static::once()) - ->method('isActive') - ->with($storeId) - ->willReturn(true); - $this->vaultPayment->expects(static::once()) + $this->vaultPaymentInstance->expects(static::once()) ->method('getProviderCode') ->willReturn($vaultProviderCode); @@ -142,8 +156,13 @@ class TokensConfigProviderTest extends \PHPUnit_Framework_TestCase $this->objectManager->setBackwardCompatibleProperty( $configProvider, - 'paymentDataHelper', - $this->paymentDataHelper + 'paymentMethodList', + $this->paymentMethodList + ); + $this->objectManager->setBackwardCompatibleProperty( + $configProvider, + 'paymentMethodInstanceFactory', + $this->paymentMethodInstanceFactory ); static::assertEquals($expectedConfig, $configProvider->getConfig()); diff --git a/app/code/Magento/Vault/Test/Unit/Model/Ui/VaultConfigProviderTest.php b/app/code/Magento/Vault/Test/Unit/Model/Ui/VaultConfigProviderTest.php index 7d0d8339ab2b1825a77041ff4f2beb0a6d686ea1..d00531637b86d4e509662a31ce2da32752ba4a8b 100644 --- a/app/code/Magento/Vault/Test/Unit/Model/Ui/VaultConfigProviderTest.php +++ b/app/code/Magento/Vault/Test/Unit/Model/Ui/VaultConfigProviderTest.php @@ -7,7 +7,6 @@ namespace Magento\Vault\Test\Unit\Model\Ui; use Magento\Customer\Model\Session; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Payment\Helper\Data; use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Vault\Model\Ui\VaultConfigProvider; @@ -21,15 +20,25 @@ use PHPUnit_Framework_MockObject_MockObject as MockObject; class VaultConfigProviderTest extends \PHPUnit_Framework_TestCase { /** - * @var Data|MockObject + * @var \Magento\Payment\Api\PaymentMethodListInterface|MockObject */ - private $paymentDataHelper; + private $paymentMethodList; /** - * @var VaultPaymentInterface|MockObject + * @var \Magento\Payment\Model\Method\InstanceFactory|MockObject + */ + private $paymentMethodInstanceFactory; + + /** + * @var \Magento\Payment\Api\Data\PaymentMethodInterface|MockObject */ private $vaultPayment; + /** + * @var VaultPaymentInterface|MockObject + */ + private $vaultPaymentInstance; + /** * @var Session|MockObject */ @@ -52,12 +61,16 @@ class VaultConfigProviderTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->paymentDataHelper = $this->getMockBuilder(Data::class) + $this->paymentMethodList = $this->getMockBuilder(\Magento\Payment\Api\PaymentMethodListInterface::class) ->disableOriginalConstructor() - ->setMethods(['getStoreMethods']) - ->getMock(); + ->getMockForAbstractClass(); - $this->vaultPayment = $this->getMockForAbstractClass(VaultPaymentInterface::class); + $this->paymentMethodInstanceFactory = $this->getMockBuilder( + \Magento\Payment\Model\Method\InstanceFactory::class + )->disableOriginalConstructor()->getMock(); + + $this->vaultPayment = $this->getMockForAbstractClass(\Magento\Payment\Api\Data\PaymentMethodInterface::class); + $this->vaultPaymentInstance = $this->getMockForAbstractClass(VaultPaymentInterface::class); $this->storeManager = $this->getMockForAbstractClass(StoreManagerInterface::class); $this->store = $this->getMockForAbstractClass(StoreInterface::class); $this->session = $this->getMockBuilder(Session::class) @@ -68,8 +81,13 @@ class VaultConfigProviderTest extends \PHPUnit_Framework_TestCase $this->vaultConfigProvider = new VaultConfigProvider($this->storeManager, $this->session); $objectManager->setBackwardCompatibleProperty( $this->vaultConfigProvider, - 'paymentDataHelper', - $this->paymentDataHelper + 'paymentMethodList', + $this->paymentMethodList + ); + $objectManager->setBackwardCompatibleProperty( + $this->vaultConfigProvider, + 'paymentMethodInstanceFactory', + $this->paymentMethodInstanceFactory ); } @@ -101,15 +119,19 @@ class VaultConfigProviderTest extends \PHPUnit_Framework_TestCase ->method('getId') ->willReturn($storeId); - $this->paymentDataHelper->expects(static::once()) - ->method('getStoreMethods') + $this->paymentMethodList->expects(static::once()) + ->method('getActiveList') ->with($storeId) ->willReturn([$this->vaultPayment]); - $this->vaultPayment->expects(static::once()) + $this->paymentMethodInstanceFactory->expects($this->once()) + ->method('create') + ->willReturn($this->vaultPaymentInstance); + + $this->vaultPaymentInstance->expects(static::once()) ->method('getCode') ->willReturn($vaultPaymentCode); - $this->vaultPayment->expects($customerId !== null ? static::once() : static::never()) + $this->vaultPaymentInstance->expects($customerId !== null ? static::once() : static::never()) ->method('isActive') ->with($storeId) ->willReturn($vaultEnabled); diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2050e01cbfdd70f5b26b40d4728d265b6b50fc77 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php @@ -0,0 +1,314 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Service\V1; + +/** + * API test for creation of Creditmemo for certain Order. + */ +class RefundOrderTest extends \Magento\TestFramework\TestCase\WebapiAbstract +{ + const SERVICE_READ_NAME = 'salesRefundOrderV1'; + const SERVICE_VERSION = 'V1'; + + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var \Magento\Sales\Api\CreditmemoRepositoryInterface + */ + private $creditmemoRepository; + + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + + $this->creditmemoRepository = $this->objectManager->get( + \Magento\Sales\Api\CreditmemoRepositoryInterface::class + ); + } + + /** + * @magentoApiDataFixture Magento/Sales/_files/order_with_shipping_and_invoice.php + */ + public function testShortRequest() + { + /** @var \Magento\Sales\Model\Order $existingOrder */ + $existingOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId('100000001'); + + $result = $this->_webApiCall( + $this->getServiceData($existingOrder), + ['orderId' => $existingOrder->getEntityId()] + ); + + $this->assertNotEmpty( + $result, + 'Failed asserting that the received response is correct' + ); + + /** @var \Magento\Sales\Model\Order $updatedOrder */ + $updatedOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId($existingOrder->getIncrementId()); + + try { + $creditmemo = $this->creditmemoRepository->get($result); + + $expectedItems = $this->getOrderItems($existingOrder); + $actualCreditmemoItems = $this->getCreditmemoItems($creditmemo); + $actualRefundedOrderItems = $this->getRefundedOrderItems($updatedOrder); + + $this->assertEquals( + $expectedItems, + $actualCreditmemoItems, + 'Failed asserting that the Creditmemo contains all requested items' + ); + + $this->assertEquals( + $expectedItems, + $actualRefundedOrderItems, + 'Failed asserting that all requested order items were refunded' + ); + + $this->assertEquals( + $creditmemo->getShippingAmount(), + $existingOrder->getShippingAmount(), + 'Failed asserting that the Creditmemo contains correct shipping amount' + ); + + $this->assertEquals( + $creditmemo->getShippingAmount(), + $updatedOrder->getShippingRefunded(), + 'Failed asserting that proper shipping amount of the Order was refunded' + ); + + $this->assertNotEquals( + $existingOrder->getStatus(), + $updatedOrder->getStatus(), + 'Failed asserting that order status was changed' + ); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + $this->fail('Failed asserting that Creditmemo was created'); + } + } + + /** + * @magentoApiDataFixture Magento/Sales/_files/order_with_shipping_and_invoice.php + */ + public function testFullRequest() + { + /** @var \Magento\Sales\Model\Order $existingOrder */ + $existingOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId('100000001'); + + $expectedItems = $this->getOrderItems($existingOrder); + $expectedItems[0]['qty'] = $expectedItems[0]['qty'] - 1; + + $expectedComment = [ + 'comment' => 'Test Comment', + 'is_visible_on_front' => 1 + ]; + + $expectedShippingAmount = 15; + $expectedAdjustmentPositive = 5.53; + $expectedAdjustmentNegative = 5.53; + + $result = $this->_webApiCall( + $this->getServiceData($existingOrder), + [ + 'orderId' => $existingOrder->getEntityId(), + 'items' => $expectedItems, + 'comment' => $expectedComment, + 'arguments' => [ + 'shipping_amount' => $expectedShippingAmount, + 'adjustment_positive' => $expectedAdjustmentPositive, + 'adjustment_negative' => $expectedAdjustmentNegative + ] + ] + ); + + $this->assertNotEmpty( + $result, + 'Failed asserting that the received response is correct' + ); + + /** @var \Magento\Sales\Model\Order $updatedOrder */ + $updatedOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId($existingOrder->getIncrementId()); + + try { + $creditmemo = $this->creditmemoRepository->get($result); + + $actualCreditmemoItems = $this->getCreditmemoItems($creditmemo); + $actualCreditmemoComment = $this->getRecentComment($creditmemo); + $actualRefundedOrderItems = $this->getRefundedOrderItems($updatedOrder); + + $this->assertEquals( + $expectedItems, + $actualCreditmemoItems, + 'Failed asserting that the Creditmemo contains all requested items' + ); + + $this->assertEquals( + $expectedItems, + $actualRefundedOrderItems, + 'Failed asserting that all requested order items were refunded' + ); + + $this->assertEquals( + $expectedComment, + $actualCreditmemoComment, + 'Failed asserting that the Creditmemo contains correct comment' + ); + + $this->assertEquals( + $expectedShippingAmount, + $creditmemo->getShippingAmount(), + 'Failed asserting that the Creditmemo contains correct shipping amount' + ); + + $this->assertEquals( + $expectedShippingAmount, + $updatedOrder->getShippingRefunded(), + 'Failed asserting that proper shipping amount of the Order was refunded' + ); + + $this->assertEquals( + $expectedAdjustmentPositive, + $creditmemo->getAdjustmentPositive(), + 'Failed asserting that the Creditmemo contains correct positive adjustment' + ); + + $this->assertEquals( + $expectedAdjustmentNegative, + $creditmemo->getAdjustmentNegative(), + 'Failed asserting that the Creditmemo contains correct negative adjustment' + ); + + $this->assertEquals( + $existingOrder->getStatus(), + $updatedOrder->getStatus(), + 'Failed asserting that order status was NOT changed' + ); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + $this->fail('Failed asserting that Creditmemo was created'); + } + } + + /** + * Prepares and returns info for API service. + * + * @param \Magento\Sales\Api\Data\OrderInterface $order + * + * @return array + */ + private function getServiceData(\Magento\Sales\Api\Data\OrderInterface $order) + { + return [ + 'rest' => [ + 'resourcePath' => '/V1/order/' . $order->getEntityId() . '/refund', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => self::SERVICE_READ_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_READ_NAME . 'execute', + ] + ]; + } + + /** + * Gets all items of given Order in proper format. + * + * @param \Magento\Sales\Model\Order $order + * + * @return array + */ + private function getOrderItems(\Magento\Sales\Model\Order $order) + { + $items = []; + + /** @var \Magento\Sales\Api\Data\OrderItemInterface $item */ + foreach ($order->getAllItems() as $item) { + $items[] = [ + 'order_item_id' => $item->getItemId(), + 'qty' => $item->getQtyOrdered(), + ]; + } + + return $items; + } + + /** + * Gets refunded items of given Order in proper format. + * + * @param \Magento\Sales\Model\Order $order + * + * @return array + */ + private function getRefundedOrderItems(\Magento\Sales\Model\Order $order) + { + $items = []; + + /** @var \Magento\Sales\Api\Data\OrderItemInterface $item */ + foreach ($order->getAllItems() as $item) { + if ($item->getQtyRefunded() > 0) { + $items[] = [ + 'order_item_id' => $item->getItemId(), + 'qty' => $item->getQtyRefunded(), + ]; + } + } + + return $items; + } + + /** + * Gets all items of given Creditmemo in proper format. + * + * @param \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo + * + * @return array + */ + private function getCreditmemoItems(\Magento\Sales\Api\Data\CreditmemoInterface $creditmemo) + { + $items = []; + + /** @var \Magento\Sales\Api\Data\CreditmemoItemInterface $item */ + foreach ($creditmemo->getItems() as $item) { + $items[] = [ + 'order_item_id' => $item->getOrderItemId(), + 'qty' => $item->getQty(), + ]; + } + + return $items; + } + + /** + * Gets the most recent comment of given Creditmemo in proper format. + * + * @param \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo + * + * @return array|null + */ + private function getRecentComment(\Magento\Sales\Api\Data\CreditmemoInterface $creditmemo) + { + $comments = $creditmemo->getComments(); + + if ($comments) { + $comment = reset($comments); + + return [ + 'comment' => $comment->getComment(), + 'is_visible_on_front' => $comment->getIsVisibleOnFront(), + ]; + } + + return null; + } +} diff --git a/dev/tests/functional/tests/app/Magento/Braintree/Test/Block/System/Config/Braintree.php b/dev/tests/functional/tests/app/Magento/Braintree/Test/Block/System/Config/Braintree.php index f46f070216c6e59456281af4ae99762898b3da71..c85c857cbeb5450ae39c29bc6658ef920e984f96 100644 --- a/dev/tests/functional/tests/app/Magento/Braintree/Test/Block/System/Config/Braintree.php +++ b/dev/tests/functional/tests/app/Magento/Braintree/Test/Block/System/Config/Braintree.php @@ -33,7 +33,7 @@ class Braintree extends Block private $enablers = [ 'Enable this Solution' => "#payment_us_braintree_section_braintree_active", 'Enable PayPal through Braintree' => '#payment_us_braintree_section_braintree_active_braintree_paypal', - 'Vault enabled' => '#payment_us_braintree_section_braintree_braintree_cc_vault_active' + 'Vault Enabled' => '#payment_us_braintree_section_braintree_braintree_cc_vault_active' ]; /** diff --git a/dev/tests/functional/tests/app/Magento/Paypal/Test/Block/System/Config/PayflowPro.php b/dev/tests/functional/tests/app/Magento/Paypal/Test/Block/System/Config/PayflowPro.php index f05092745c7608ca1a83bdc54fba527d116e95cb..e3b01aa3af9a0dfa4517ad3fe4b527eec9d30ad0 100644 --- a/dev/tests/functional/tests/app/Magento/Paypal/Test/Block/System/Config/PayflowPro.php +++ b/dev/tests/functional/tests/app/Magento/Paypal/Test/Block/System/Config/PayflowPro.php @@ -42,7 +42,7 @@ class PayflowPro extends Block '_payflow_required_enable_paypal_payflow', 'Enable PayPal Credit' => '#payment_us_paypal_payment_gateways_paypal_payflowpro_with_express_checkout_paypal' . '_payflow_required_enable_express_checkout_bml_payflow', - 'Vault enabled' => '#payment_us_paypal_payment_gateways_paypal_payflowpro_with_express_checkout_paypal_' . + 'Vault Enabled' => '#payment_us_paypal_payment_gateways_paypal_payflowpro_with_express_checkout_paypal_' . 'payflow_required_payflowpro_cc_vault_active' ]; diff --git a/dev/tests/functional/tests/app/Magento/Paypal/Test/Block/System/Config/PaymentsPro.php b/dev/tests/functional/tests/app/Magento/Paypal/Test/Block/System/Config/PaymentsPro.php index 709dc4b54b53f275b222a1cc2981bce88f7fb605..cd146aa8f809cf5de0cbddcb02be83a19e59928f 100644 --- a/dev/tests/functional/tests/app/Magento/Paypal/Test/Block/System/Config/PaymentsPro.php +++ b/dev/tests/functional/tests/app/Magento/Paypal/Test/Block/System/Config/PaymentsPro.php @@ -42,7 +42,7 @@ class PaymentsPro extends Block '_payflow', 'Enable PayPal Credit' => '#payment_us_paypal_group_all_in_one_wpp_usuk_paypal_payflow_required_enable_' . 'express_checkout_bml_payflow', - 'Vault enabled' => '#payment_us_paypal_group_all_in_one_wpp_usuk_paypal_payflow_required_payflowpro_cc_vault' . + 'Vault Enabled' => '#payment_us_paypal_group_all_in_one_wpp_usuk_paypal_payflow_required_payflowpro_cc_vault' . '_active' ]; diff --git a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestStep/CheckPayflowProConfigStep.php b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestStep/CheckPayflowProConfigStep.php index a468fbde2f4d252997f1f84973063b397ea074b5..7e0c9d934161eb36d8b16454d8b263978cd6c21a 100644 --- a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestStep/CheckPayflowProConfigStep.php +++ b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestStep/CheckPayflowProConfigStep.php @@ -130,7 +130,7 @@ class CheckPayflowProConfigStep implements TestStepInterface $this->payflowProConfigBlock->enablePayflowPro(); $this->assertFieldsAreActive->processAssert( $this->systemConfigEditSectionPayment, - [$enablers['Enable this Solution'], $enablers['Enable PayPal Credit'], $enablers['Vault enabled']] + [$enablers['Enable this Solution'], $enablers['Enable PayPal Credit'], $enablers['Vault Enabled']] ); $this->assertFieldsAreEnabled->processAssert( $this->systemConfigEditSectionPayment, diff --git a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestStep/CheckPaymentsProConfigStep.php b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestStep/CheckPaymentsProConfigStep.php index 2555eab030720c475f56aa28181607a0be452fac..4c3318d14f9611a7890e8621e29661cafc1ca620 100644 --- a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestStep/CheckPaymentsProConfigStep.php +++ b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestStep/CheckPaymentsProConfigStep.php @@ -130,7 +130,7 @@ class CheckPaymentsProConfigStep implements TestStepInterface $this->paymentsProConfigBlock->enablePaymentsPro(); $this->assertFieldsAreActive->processAssert( $this->systemConfigEditSectionPayment, - [$enablers['Enable this Solution'], $enablers['Enable PayPal Credit'], $enablers['Vault enabled']] + [$enablers['Enable this Solution'], $enablers['Enable PayPal Credit'], $enablers['Vault Enabled']] ); $this->assertFieldsAreEnabled->processAssert( $this->systemConfigEditSectionPayment, diff --git a/dev/tests/functional/tests/app/Magento/Setup/Test/Block/SelectVersion.php b/dev/tests/functional/tests/app/Magento/Setup/Test/Block/SelectVersion.php index 0ce9dd2867a13fe09cc24bfb11fe6dcd404a2522..437203e072e9626465f6ba9c2a44dd7e97551695 100644 --- a/dev/tests/functional/tests/app/Magento/Setup/Test/Block/SelectVersion.php +++ b/dev/tests/functional/tests/app/Magento/Setup/Test/Block/SelectVersion.php @@ -30,6 +30,13 @@ class SelectVersion extends Form */ protected $firstField = '#selectVersion'; + /** + * Show all versions checkbox + * + * @var string + */ + private $showAllVersions = '#showUnstable'; + /** * Click on 'Next' button. * @@ -50,9 +57,24 @@ class SelectVersion extends Form public function fill(FixtureInterface $fixture, SimpleElement $element = null) { $this->waitForElementVisible($this->firstField); + $this->chooseShowAllVersions(); + return parent::fill($fixture, $element); } + /** + * Show all versions include unstable + * + * @return void + */ + private function chooseShowAllVersions() + { + $element = $this->_rootElement->find($this->showAllVersions, Locator::SELECTOR_CSS); + if ($element->isVisible()) { + $element->click(); + } + } + /** * Choose 'yes' for upgrade option called 'Other components' * diff --git a/dev/tests/functional/tests/app/Magento/Setup/Test/Constraint/AssertSuccessMessage.php b/dev/tests/functional/tests/app/Magento/Setup/Test/Constraint/AssertSuccessMessage.php index 7f7d17f60679c3ef1a04810505eaab0ad7533bf2..dcfb2922d5c787150487ecf95542ed9d24411b64 100644 --- a/dev/tests/functional/tests/app/Magento/Setup/Test/Constraint/AssertSuccessMessage.php +++ b/dev/tests/functional/tests/app/Magento/Setup/Test/Constraint/AssertSuccessMessage.php @@ -23,7 +23,7 @@ class AssertSuccessMessage extends AbstractConstraint */ public function processAssert(SetupWizard $setupWizard, $package) { - $message = "You upgraded:"; + $message = "You upgraded"; \PHPUnit_Framework_Assert::assertContains( $message, $setupWizard->getSuccessMessage()->getUpdaterStatus(), diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/setup.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/setup.xml index 6a5c6e5d904ddb2b9de92ab7f851f97ffad7db44..e1f1395bc132c892f884ceb170dd3ade6bdf525b 100644 --- a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/setup.xml +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/setup.xml @@ -11,5 +11,8 @@ <allow> <module value="Magento_Setup"/> </allow> + <deny> + <class value="Magento\Setup\Test\TestCase\UpgradeSystemTest"/> + </deny> </rule> </config> diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/upgrade.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/upgrade.xml new file mode 100644 index 0000000000000000000000000000000000000000..71f50859db921163bf50a8f85d79e96c58f288a6 --- /dev/null +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/upgrade.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../vendor/magento/mtf/Magento/Mtf/TestRunner/etc/testRunner.xsd"> + <rule scope="testsuite"> + <allow> + <class value="Magento\Setup\Test\TestCase\UpgradeSystemTest"/> + </allow> + </rule> +</config> \ No newline at end of file diff --git a/dev/tests/integration/testsuite/Magento/Framework/View/Asset/MinifierTest.php b/dev/tests/integration/testsuite/Magento/Framework/View/Asset/MinifierTest.php index f90fc157e37296e61d1dd15f9254d1803edb8752..c155ef8a5c9c9ee9ba708c3f74e33331ce330aaf 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/View/Asset/MinifierTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/View/Asset/MinifierTest.php @@ -140,43 +140,44 @@ class MinifierTest extends \PHPUnit_Framework_TestCase } /** - * @magentoConfigFixture current_store dev/css/minify_files 1 + * @magentoConfigFixture current_store dev/css/minify_files 0 + * @magentoAppIsolation enabled */ - public function testCssMinification() + public function testCssMinificationOff() { $this->_testCssMinification( - '/frontend/FrameworkViewMinifier/default/en_US/css/styles.min.css', + '/frontend/FrameworkViewMinifier/default/en_US/css/styles.css', function ($path) { - $this->assertEquals( + $content = file_get_contents($path); + $this->assertNotEmpty($content); + $this->assertContains('FrameworkViewMinifier/frontend', $content); + $this->assertNotEquals( file_get_contents( dirname(__DIR__) . '/_files/static/expected/styles.magento.min.css' ), - file_get_contents($path), - 'Minified files are not equal or minification did not work for requested CSS' + $content, + 'CSS is minified when minification turned off' ); } ); } /** - * @magentoConfigFixture current_store dev/css/minify_files 0 + * @magentoConfigFixture current_store dev/css/minify_files 1 */ - public function testCssMinificationOff() + public function testCssMinification() { $this->_testCssMinification( - '/frontend/FrameworkViewMinifier/default/en_US/css/styles.css', + '/frontend/FrameworkViewMinifier/default/en_US/css/styles.min.css', function ($path) { - $content = file_get_contents($path); - $this->assertNotEmpty($content); - $this->assertContains('FrameworkViewMinifier/frontend', $content); - $this->assertNotEquals( + $this->assertEquals( file_get_contents( dirname(__DIR__) . '/_files/static/expected/styles.magento.min.css' ), - $content, - 'CSS is minified when minification turned off' + file_get_contents($path), + 'Minified files are not equal or minification did not work for requested CSS' ); } ); @@ -234,13 +235,13 @@ class MinifierTest extends \PHPUnit_Framework_TestCase ] )); - /** @var \Magento\Deploy\Model\Deployer $deployer */ + /** @var \Magento\Deploy\Model\Deploy\LocaleDeploy $deployer */ $deployer = $this->objectManager->create( - \Magento\Deploy\Model\Deployer::class, + \Magento\Deploy\Model\Deploy\LocaleDeploy::class, ['filesUtil' => $filesUtil, 'output' => $output] ); - $deployer->deploy($omFactory, ['en_US'], ['frontend' => ['FrameworkViewMinifier/default']]); + $deployer->deploy('frontend', 'FrameworkViewMinifier/default', 'en_US', []); $this->assertFileExists($fileToBePublished); $this->assertEquals( diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/_files/expected/config.xml b/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/_files/expected/config.xml index 40bf9602edb12ad06b710839876d7af438b5a831..f9f8f1ffba91a55536c9d2221b68dc85b702e7f0 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/_files/expected/config.xml +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/_files/expected/config.xml @@ -115,7 +115,7 @@ </requires> </field> <field id="payflowpro_cc_vault_active" translate="label" type="select" sortOrder="22" showInDefault="1" showInWebsite="1" showInStore="0"> - <label>Vault enabled</label> + <label>Vault Enabled</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <config_path>payment/payflowpro_cc_vault/active</config_path> <attribute type="shared">1</attribute> diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice.php new file mode 100644 index 0000000000000000000000000000000000000000..3dfa428c4aad48ee2bfbd8eadbc10fc516090d67 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +require 'order.php'; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var \Magento\Sales\Model\Order $order */ +$order = $objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId('100000001'); + +/** @var \Magento\Sales\Model\Service\InvoiceService $invoiceService */ +$invoiceService = $objectManager->create(\Magento\Sales\Api\InvoiceManagementInterface::class); + +/** @var \Magento\Framework\DB\Transaction $transaction */ +$transaction = $objectManager->create(\Magento\Framework\DB\Transaction::class); + +$order->setData( + 'base_to_global_rate', + 1 +)->setData( + 'base_to_order_rate', + 1 +)->setData( + 'shipping_amount', + 20 +)->setData( + 'base_shipping_amount', + 20 +); + +$invoice = $invoiceService->prepareInvoice($order); +$invoice->register(); + +$order->setIsInProcess(true); + +$transaction->addObject($invoice)->addObject($order)->save(); diff --git a/lib/internal/Magento/Framework/App/Config/ScopePool.php b/lib/internal/Magento/Framework/App/Config/ScopePool.php index d366349722f0fe8157be6b21ad1a290d17cf67b2..9e6a47d918f7602cd2e2b1d50286eb82f3f5cca6 100644 --- a/lib/internal/Magento/Framework/App/Config/ScopePool.php +++ b/lib/internal/Magento/Framework/App/Config/ScopePool.php @@ -92,16 +92,18 @@ class ScopePool { $scopeCode = $this->_getScopeCode($scopeType, $scopeCode); - // Key by url to support dynamic {{base_url}} and port assignments - $host = $this->getRequest()->getHttpHost(); - $port = $this->getRequest()->getServer('SERVER_PORT'); - $path = $this->getRequest()->getBasePath(); - $urlInfo = $host . $port . trim($path, '/'); - $code = $scopeType . '|' . $scopeCode . '|' . $urlInfo; + $code = $scopeType . '|' . $scopeCode; if (!isset($this->_scopes[$code])) { - $cacheKey = $this->_cacheId . '|' . $code; + // Key by url to support dynamic {{base_url}} and port assignments + $host = $this->getRequest()->getHttpHost(); + $port = $this->getRequest()->getServer('SERVER_PORT'); + $path = $this->getRequest()->getBasePath(); + + $urlInfo = $host . $port . trim($path, '/'); + $cacheKey = $this->_cacheId . '|' . $code . '|' . $urlInfo; $data = $this->_cache->load($cacheKey); + if ($data) { $data = unserialize($data); } else { diff --git a/lib/internal/Magento/Framework/App/ResourceConnection.php b/lib/internal/Magento/Framework/App/ResourceConnection.php index 5a2e4a6710a187d3506dc96020e7f17209513ff6..d41a457d4559cf077259f70210a2318e61f7d93f 100644 --- a/lib/internal/Magento/Framework/App/ResourceConnection.php +++ b/lib/internal/Magento/Framework/App/ResourceConnection.php @@ -92,6 +92,18 @@ class ResourceConnection return $this->getConnectionByName($connectionName); } + /** + * @param string $resourceName + * @return void + */ + public function closeConnection($resourceName = self::DEFAULT_CONNECTION) + { + $processConnectionName = $this->getProcessConnectionName($this->config->getConnectionName($resourceName)); + if (isset($this->connections[$processConnectionName])) { + $this->connections[$processConnectionName] = null; + } + } + /** * Retrieve connection by $connectionName * @@ -101,8 +113,9 @@ class ResourceConnection */ public function getConnectionByName($connectionName) { - if (isset($this->connections[$connectionName])) { - return $this->connections[$connectionName]; + $processConnectionName = $this->getProcessConnectionName($connectionName); + if (isset($this->connections[$processConnectionName])) { + return $this->connections[$processConnectionName]; } $connectionConfig = $this->deploymentConfig->get( @@ -115,10 +128,19 @@ class ResourceConnection throw new \DomainException('Connection "' . $connectionName . '" is not defined'); } - $this->connections[$connectionName] = $connection; + $this->connections[$processConnectionName] = $connection; return $connection; } + /** + * @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/Css/PreProcessor/Adapter/Less/Processor.php b/lib/internal/Magento/Framework/Css/PreProcessor/Adapter/Less/Processor.php index 49d2545ea5bb807bdd7853047e0496966e5ba177..5f2414283ad89a8dd62ebca632e95844ed9a6575 100644 --- a/lib/internal/Magento/Framework/Css/PreProcessor/Adapter/Less/Processor.php +++ b/lib/internal/Magento/Framework/Css/PreProcessor/Adapter/Less/Processor.php @@ -81,9 +81,11 @@ class Processor implements ContentProcessorInterface } $tmpFilePath = $this->temporaryFile->createFile($path, $content); - $parser->parseFile($tmpFilePath, ''); + gc_disable(); + $parser->parseFile($tmpFilePath, ''); $content = $parser->getCss(); + gc_enable(); if (trim($content) === '') { $errorMessage = PHP_EOL . self::ERROR_MESSAGE_PREFIX . PHP_EOL . $path; diff --git a/lib/internal/Magento/Framework/Css/PreProcessor/Instruction/MagentoImport.php b/lib/internal/Magento/Framework/Css/PreProcessor/Instruction/MagentoImport.php index a07c7eea16aeb452aac3b1561b313bf2296a51fb..ab5db005978b1ec4e96ee8295a2a75a8cd5986b5 100644 --- a/lib/internal/Magento/Framework/Css/PreProcessor/Instruction/MagentoImport.php +++ b/lib/internal/Magento/Framework/Css/PreProcessor/Instruction/MagentoImport.php @@ -5,15 +5,18 @@ */ namespace Magento\Framework\Css\PreProcessor\Instruction; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Css\PreProcessor\ErrorHandlerInterface; use Magento\Framework\View\Asset\File\FallbackContext; use Magento\Framework\View\Asset\LocalInterface; use Magento\Framework\View\Asset\PreProcessorInterface; +use Magento\Framework\View\Design\Theme\ThemeProviderInterface; use Magento\Framework\View\DesignInterface; use Magento\Framework\View\File\CollectorInterface; /** * @magento_import instruction preprocessor + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Must be deleted after moving themeProvider to construct */ class MagentoImport implements PreProcessorInterface { @@ -45,9 +48,15 @@ class MagentoImport implements PreProcessorInterface /** * @var \Magento\Framework\View\Design\Theme\ListInterface + * @deprecated */ protected $themeList; + /** + * @var ThemeProviderInterface + */ + private $themeProvider; + /** * @param DesignInterface $design * @param CollectorInterface $fileSource @@ -120,8 +129,23 @@ class MagentoImport implements PreProcessorInterface { $context = $asset->getContext(); if ($context instanceof FallbackContext) { - return $this->themeList->getThemeByFullPath($context->getAreaCode() . '/' . $context->getThemePath()); + return $this->getThemeProvider()->getThemeByFullPath( + $context->getAreaCode() . '/' . $context->getThemePath() + ); } return $this->design->getDesignTheme(); } + + /** + * @return ThemeProviderInterface + * @deprecated + */ + private function getThemeProvider() + { + if (null === $this->themeProvider) { + $this->themeProvider = ObjectManager::getInstance()->get(ThemeProviderInterface::class); + } + + return $this->themeProvider; + } } diff --git a/lib/internal/Magento/Framework/Css/Test/Unit/PreProcessor/Instruction/MagentoImportTest.php b/lib/internal/Magento/Framework/Css/Test/Unit/PreProcessor/Instruction/MagentoImportTest.php index f5c81d78436aa40e8218d22c6af90a3c388e1891..9bc04c8ff33e9183deebee50faa08765949503d4 100644 --- a/lib/internal/Magento/Framework/Css/Test/Unit/PreProcessor/Instruction/MagentoImportTest.php +++ b/lib/internal/Magento/Framework/Css/Test/Unit/PreProcessor/Instruction/MagentoImportTest.php @@ -8,6 +8,10 @@ namespace Magento\Framework\Css\Test\Unit\PreProcessor\Instruction; +use Magento\Framework\Css\PreProcessor\Instruction\MagentoImport; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\View\Design\Theme\ThemeProviderInterface; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -39,9 +43,9 @@ class MagentoImportTest extends \PHPUnit_Framework_TestCase private $assetRepo; /** - * @var \Magento\Framework\View\Design\Theme\ListInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ThemeProviderInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $themeList; + private $themeProvider; /** * @var \Magento\Framework\Css\PreProcessor\Instruction\Import @@ -58,14 +62,14 @@ class MagentoImportTest extends \PHPUnit_Framework_TestCase $this->asset = $this->getMock(\Magento\Framework\View\Asset\File::class, [], [], '', false); $this->asset->expects($this->any())->method('getContentType')->will($this->returnValue('css')); $this->assetRepo = $this->getMock(\Magento\Framework\View\Asset\Repository::class, [], [], '', false); - $this->themeList = $this->getMockForAbstractClass(\Magento\Framework\View\Design\Theme\ListInterface::class); - $this->object = new \Magento\Framework\Css\PreProcessor\Instruction\MagentoImport( - $this->design, - $this->fileSource, - $this->errorHandler, - $this->assetRepo, - $this->themeList - ); + $this->themeProvider = $this->getMock(ThemeProviderInterface::class); + $this->object = (new ObjectManager($this))->getObject(MagentoImport::class, [ + 'design' => $this->design, + 'fileSource' => $this->fileSource, + 'errorHandler' => $this->errorHandler, + 'assetRepo' => $this->assetRepo, + 'themeProvider' => $this->themeProvider + ]); } /** @@ -91,7 +95,7 @@ class MagentoImportTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue($relatedAsset)); $relatedAsset->expects($this->once())->method('getContext')->will($this->returnValue($context)); $theme = $this->getMockForAbstractClass(\Magento\Framework\View\Design\ThemeInterface::class); - $this->themeList->expects($this->once())->method('getThemeByFullPath')->will($this->returnValue($theme)); + $this->themeProvider->expects($this->once())->method('getThemeByFullPath')->will($this->returnValue($theme)); $files = []; foreach ($foundFiles as $file) { $fileObject = $this->getMock(\Magento\Framework\View\File::class, [], [], '', false); diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php index 1f69922b799893068da3d91958ca8d53f79e0424..b7752ddccf886b391e23b121ba29fa324348e68c 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php @@ -144,8 +144,6 @@ class Write extends Read implements WriteInterface */ public function createSymlink($path, $destination, WriteInterface $targetDirectory = null) { - $this->assertIsFile($path); - $targetDirectory = $targetDirectory ?: $this; $parentDirectory = $this->driver->getParentDirectory($destination); if (!$targetDirectory->isExist($parentDirectory)) { diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/WriteInterface.php b/lib/internal/Magento/Framework/Filesystem/Directory/WriteInterface.php index 472c54a08d202d2acfe5d0150949a5e254f04447..3fa0513dd21a0b7a5cd279204547b0aa4069879c 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/WriteInterface.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/WriteInterface.php @@ -48,7 +48,7 @@ interface WriteInterface extends ReadInterface public function copyFile($path, $destination, WriteInterface $targetDirectory = null); /** - * Creates symlink on a file and places it to destination + * Creates symlink on a file or directory and places it to destination * * @param string $path * @param string $destination diff --git a/lib/internal/Magento/Framework/Test/Unit/App/ResourceConnectionTest.php b/lib/internal/Magento/Framework/Test/Unit/App/ResourceConnectionTest.php new file mode 100644 index 0000000000000000000000000000000000000000..9cbdea93cf4fcd549b26731e670741ed8391be36 --- /dev/null +++ b/lib/internal/Magento/Framework/Test/Unit/App/ResourceConnectionTest.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Framework\Test\Unit\App; + +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Config\ConfigOptionsListConstants; +use Magento\Framework\Model\ResourceModel\Type\Db\ConnectionFactoryInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\App\ResourceConnection\ConfigInterface; + +class ResourceConnectionTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var ResourceConnection + */ + private $unit; + + /** + * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + private $deploymentConfigMock; + + /** + * @var ConnectionFactoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $connectionFactoryMock; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + protected function setUp() + { + $this->deploymentConfigMock = $this->getMockBuilder(DeploymentConfig::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->connectionFactoryMock = $this->getMockBuilder(ConnectionFactoryInterface::class) + ->getMock(); + + $this->configMock = $this->getMockBuilder(ConfigInterface::class)->getMock(); + + $this->objectManager = (new ObjectManager($this)); + $this->unit = $this->objectManager->getObject( + ResourceConnection::class, + [ + 'deploymentConfig' => $this->deploymentConfigMock, + 'connectionFactory' => $this->connectionFactoryMock, + 'config' => $this->configMock, + ] + ); + } + + public function testGetConnectionByName() + { + $this->deploymentConfigMock->expects(self::once())->method('get') + ->with(ConfigOptionsListConstants::CONFIG_PATH_DB_CONNECTIONS . '/default') + ->willReturn(['config']); + $this->connectionFactoryMock->expects(self::once())->method('create') + ->with(['config']) + ->willReturn('connection'); + + self::assertEquals('connection', $this->unit->getConnectionByName('default')); + } + + public function testGetExistingConnectionByName() + { + $unit = $this->objectManager->getObject( + ResourceConnection::class, + [ + 'deploymentConfig' => $this->deploymentConfigMock, + 'connections' => ['default_process_' . getmypid() => 'existing_connection'] + ] + ); + $this->deploymentConfigMock->expects(self::never())->method('get'); + + self::assertEquals('existing_connection', $unit->getConnectionByName('default')); + } + + public function testCloseConnection() + { + $this->configMock->expects(self::once())->method('getConnectionName')->with('default'); + + $this->unit->closeConnection('default'); + + } +} diff --git a/lib/internal/Magento/Framework/View/Asset/Bundle/Config.php b/lib/internal/Magento/Framework/View/Asset/Bundle/Config.php index 21e974fca57e01b3154c614a6cb64f5f52850e2e..0fe15fd14af709d151259209872ebfeacb46bff0 100644 --- a/lib/internal/Magento/Framework/View/Asset/Bundle/Config.php +++ b/lib/internal/Magento/Framework/View/Asset/Bundle/Config.php @@ -6,10 +6,12 @@ namespace Magento\Framework\View\Asset\Bundle; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View; use Magento\Framework\View\Asset\Bundle; use Magento\Framework\View\Design\Theme\ListInterface; use Magento\Framework\View\Asset\File\FallbackContext; +use Magento\Framework\View\Design\Theme\ThemeProviderInterface; class Config implements Bundle\ConfigInterface { @@ -30,6 +32,16 @@ class Config implements Bundle\ConfigInterface */ protected $viewConfig; + /** + * @var ThemeProviderInterface + */ + private $themeProvider; + + /** + * @var \Magento\Framework\Config\View[] + */ + private $config = []; + /** * @param View\ConfigInterface $viewConfig * @param ListInterface $themeList @@ -57,12 +69,17 @@ class Config implements Bundle\ConfigInterface */ public function getConfig(FallbackContext $assetContext) { - return $this->viewConfig->getViewConfig([ - 'area' => $assetContext->getAreaCode(), - 'themeModel' => $this->themeList->getThemeByFullPath( - $assetContext->getAreaCode() . '/' . $assetContext->getThemePath() - ) - ]); + $themePath = $assetContext->getAreaCode() . '/' . $assetContext->getThemePath(); + if (!isset($this->config[$themePath])) { + $this->config[$themePath] = $this->viewConfig->getViewConfig([ + 'area' => $assetContext->getAreaCode(), + 'themeModel' => $this->getThemeProvider()->getThemeByFullPath( + $themePath + ) + ]); + } + + return $this->config[$themePath]; } /** @@ -83,7 +100,19 @@ class Config implements Bundle\ConfigInterface case 'MB': return (int)$size * 1024; default: - return (int)$size / 1024; + return (int)($size / 1024); } } + + /** + * @return ThemeProviderInterface + */ + private function getThemeProvider() + { + if (null === $this->themeProvider) { + $this->themeProvider = ObjectManager::getInstance()->get(ThemeProviderInterface::class); + } + + return $this->themeProvider; + } } diff --git a/lib/internal/Magento/Framework/View/Asset/LockerProcess.php b/lib/internal/Magento/Framework/View/Asset/LockerProcess.php index 660cf0418669ead047cb6713883d0a8039ab75bb..e8938211fecbf642ff3994515730e517013f1100 100644 --- a/lib/internal/Magento/Framework/View/Asset/LockerProcess.php +++ b/lib/internal/Magento/Framework/View/Asset/LockerProcess.php @@ -59,7 +59,6 @@ class LockerProcess implements LockerProcessInterface /** * @inheritdoc - * @throws FileSystemException */ public function lockProcess($lockName) { @@ -94,14 +93,18 @@ class LockerProcess implements LockerProcessInterface * Check whether generation process has already locked * * @return bool - * @throws FileSystemException */ private function isProcessLocked() { if ($this->tmpDirectory->isExist($this->lockFilePath)) { - $lockTime = (int) $this->tmpDirectory->readFile($this->lockFilePath); - if ((time() - $lockTime) >= self::MAX_LOCK_TIME) { - $this->tmpDirectory->delete($this->lockFilePath); + try { + $lockTime = (int)$this->tmpDirectory->readFile($this->lockFilePath); + if ((time() - $lockTime) >= self::MAX_LOCK_TIME) { + $this->tmpDirectory->delete($this->lockFilePath); + + return false; + } + } catch (FileSystemException $e) { return false; } diff --git a/lib/internal/Magento/Framework/View/Asset/Minification.php b/lib/internal/Magento/Framework/View/Asset/Minification.php index 255c9690e3fa9f2515fc5ac50d97fb002b87ede8..1e32e32b99676e16e538445ae122221ff5e48ec6 100644 --- a/lib/internal/Magento/Framework/View/Asset/Minification.php +++ b/lib/internal/Magento/Framework/View/Asset/Minification.php @@ -21,18 +21,21 @@ class Minification * @var ScopeConfigInterface */ private $scopeConfig; + /** * @var State */ private $appState; + /** * @var string */ private $scope; + /** * @var array */ - private $excludes = []; + private $configCache = []; /** * @param ScopeConfigInterface $scopeConfig @@ -54,12 +57,16 @@ class Minification */ public function isEnabled($contentType) { - return - $this->appState->getMode() != State::MODE_DEVELOPER && - (bool)$this->scopeConfig->isSetFlag( - sprintf(self::XML_PATH_MINIFICATION_ENABLED, $contentType), - $this->scope - ); + if (!isset($this->configCache[self::XML_PATH_MINIFICATION_ENABLED][$contentType])) { + $this->configCache[self::XML_PATH_MINIFICATION_ENABLED][$contentType] = + $this->appState->getMode() != State::MODE_DEVELOPER && + (bool)$this->scopeConfig->isSetFlag( + sprintf(self::XML_PATH_MINIFICATION_ENABLED, $contentType), + $this->scope + ); + } + + return $this->configCache[self::XML_PATH_MINIFICATION_ENABLED][$contentType]; } /** @@ -131,15 +138,15 @@ class Minification */ public function getExcludes($contentType) { - if (!isset($this->excludes[$contentType])) { - $this->excludes[$contentType] = []; + if (!isset($this->configCache[self::XML_PATH_MINIFICATION_EXCLUDES][$contentType])) { + $this->configCache[self::XML_PATH_MINIFICATION_EXCLUDES][$contentType] = []; $key = sprintf(self::XML_PATH_MINIFICATION_EXCLUDES, $contentType); foreach (explode("\n", $this->scopeConfig->getValue($key, $this->scope)) as $exclude) { if (trim($exclude) != '') { - $this->excludes[$contentType][] = trim($exclude); + $this->configCache[self::XML_PATH_MINIFICATION_EXCLUDES][$contentType][] = trim($exclude); } }; } - return $this->excludes[$contentType]; + return $this->configCache[self::XML_PATH_MINIFICATION_EXCLUDES][$contentType]; } } diff --git a/lib/internal/Magento/Framework/View/Asset/NotationResolver/Variable.php b/lib/internal/Magento/Framework/View/Asset/NotationResolver/Variable.php index 59108ea0f5c427acfe367344fbfe7b6f9dcdcfaf..4e6bba487a1b7388f49483e43049892660836b14 100644 --- a/lib/internal/Magento/Framework/View/Asset/NotationResolver/Variable.php +++ b/lib/internal/Magento/Framework/View/Asset/NotationResolver/Variable.php @@ -58,18 +58,21 @@ class Variable } /** - * Retrieves the value of a given placeholder + * Process placeholder * * @param string $placeholder * @return string */ public function getPlaceholderValue($placeholder) { + /** @var \Magento\Framework\View\Asset\File\FallbackContext $context */ $context = $this->assetRepo->getStaticViewFileContext(); switch ($placeholder) { case self::VAR_BASE_URL_PATH: - return $context->getBaseUrl() . $context->getPath(); + return '{{' . self::VAR_BASE_URL_PATH . '}}' . $context->getAreaCode() . + ($context->getThemePath() ? '/' . $context->getThemePath() . '/' : '') . + '{{locale}}'; default: return ''; } diff --git a/lib/internal/Magento/Framework/View/Asset/Repository.php b/lib/internal/Magento/Framework/View/Asset/Repository.php index 072b3361cbefea99e2ebb5dd958cc8468bbda088..c898d57c11e7a6a26f205f1288097d4bb5a9a60c 100644 --- a/lib/internal/Magento/Framework/View/Asset/Repository.php +++ b/lib/internal/Magento/Framework/View/Asset/Repository.php @@ -8,7 +8,8 @@ namespace Magento\Framework\View\Asset; use Magento\Framework\UrlInterface; use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\Filesystem; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Design\Theme\ThemeProviderInterface; /** * A repository service for view assets @@ -35,6 +36,7 @@ class Repository /** * @var \Magento\Framework\View\Design\Theme\ListInterface + * @deprecated */ private $themeList; @@ -72,11 +74,17 @@ class Repository * @var File\ContextFactory */ private $contextFactory; + /** * @var RemoteFactory */ private $remoteFactory; + /** + * @var ThemeProviderInterface + */ + private $themeProvider; + /** * @param \Magento\Framework\UrlInterface $baseUrl * @param \Magento\Framework\View\DesignInterface $design @@ -138,7 +146,7 @@ class Repository } if ($theme) { - $params['themeModel'] = $this->themeList->getThemeByFullPath($area . '/' . $theme); + $params['themeModel'] = $this->getThemeProvider()->getThemeByFullPath($area . '/' . $theme); if (!$params['themeModel']) { throw new \UnexpectedValueException("Could not find theme '$theme' for area '$area'"); } @@ -158,6 +166,18 @@ class Repository return $this; } + /** + * @return ThemeProviderInterface + */ + private function getThemeProvider() + { + if (null === $this->themeProvider) { + $this->themeProvider = ObjectManager::getInstance()->get(ThemeProviderInterface::class); + } + + return $this->themeProvider; + } + /** * Get default design parameter * diff --git a/lib/internal/Magento/Framework/View/Asset/Source.php b/lib/internal/Magento/Framework/View/Asset/Source.php index 1bde94402d1a868c80998329f3a6ccf8cc67ea1f..cf80bd0d1b3821cc3050cd94ee691de98e8385e4 100644 --- a/lib/internal/Magento/Framework/View/Asset/Source.php +++ b/lib/internal/Magento/Framework/View/Asset/Source.php @@ -8,8 +8,10 @@ namespace Magento\Framework\View\Asset; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem\Directory\ReadFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View\Asset\PreProcessor\ChainFactoryInterface; use Magento\Framework\View\Design\FileResolution\Fallback\Resolver\Simple; +use Magento\Framework\View\Design\Theme\ThemeProviderInterface; /** * A service for preprocessing content of assets @@ -45,6 +47,7 @@ class Source /** * @var \Magento\Framework\View\Design\Theme\ListInterface + * @deprecated */ private $themeList; @@ -58,6 +61,11 @@ class Source */ private $readFactory; + /** + * @var ThemeProviderInterface + */ + private $themeProvider; + /** * Constructor * @@ -218,7 +226,9 @@ class Source LocalInterface $asset, \Magento\Framework\View\Asset\File\FallbackContext $context ) { - $themeModel = $this->themeList->getThemeByFullPath($context->getAreaCode() . '/' . $context->getThemePath()); + $themeModel = $this->getThemeProvider()->getThemeByFullPath( + $context->getAreaCode() . '/' . $context->getThemePath() + ); $sourceFile = $this->fallback->getFile( $context->getAreaCode(), $themeModel, @@ -229,6 +239,18 @@ class Source return $sourceFile; } + /** + * @return ThemeProviderInterface + */ + private function getThemeProvider() + { + if (null === $this->themeProvider) { + $this->themeProvider = ObjectManager::getInstance()->get(ThemeProviderInterface::class); + } + + return $this->themeProvider; + } + /** * Find asset file by simply appending its path to the directory in context * diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Asset/NotationResolver/VariableTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Asset/NotationResolver/VariableTest.php index 77b81faf359aa316efb6415d3f2f506732d70169..b4bda4a93dfd51140e6cde6f6bf8e7fab5119b5a 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Asset/NotationResolver/VariableTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Asset/NotationResolver/VariableTest.php @@ -6,43 +6,51 @@ namespace Magento\Framework\View\Test\Unit\Asset\NotationResolver; -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\View\Asset\NotationResolver; +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Asset\File\FallbackContext; +use Magento\Framework\View\Asset\NotationResolver\Variable; +use Magento\Framework\View\Asset\Repository; class VariableTest extends \PHPUnit_Framework_TestCase { /** - * @var \Magento\Framework\View\Asset\File\Context|\PHPUnit_Framework_MockObject_MockObject + * @var FallbackContext|\PHPUnit_Framework_MockObject_MockObject */ private $context; /** - * @var \Magento\Framework\View\Asset\Repository|\PHPUnit_Framework_MockObject_MockObject + * @var Repository|\PHPUnit_Framework_MockObject_MockObject */ private $assetRepo; /** - * @var \Magento\Framework\View\Asset\NotationResolver\Variable + * @var Variable */ private $object; protected function setUp() { - $baseUrl = 'http://example.com/pub/static/'; - $path = 'frontend/Magento/blank/en_US'; + $area = 'frontend'; + $themePath = 'Magento/blank'; - $this->context = $this->getMock( - \Magento\Framework\View\Asset\File\Context::class, - null, - [$baseUrl, DirectoryList::STATIC_VIEW, $path] - ); + $this->context = $this->getMockBuilder(FallbackContext::class) + ->disableOriginalConstructor() + ->getMock(); + $this->context->expects($this->once()) + ->method('getAreaCode') + ->willReturn($area); + $this->context->expects($this->exactly(2)) + ->method('getThemePath') + ->willReturn($themePath); - $this->assetRepo = $this->getMock(\Magento\Framework\View\Asset\Repository::class, [], [], '', false); + $this->assetRepo = $this->getMockBuilder(Repository::class) + ->disableOriginalConstructor() + ->getMock(); $this->assetRepo->expects($this->any()) ->method('getStaticViewFileContext') ->will($this->returnValue($this->context)); - $this->object = new \Magento\Framework\View\Asset\NotationResolver\Variable($this->assetRepo); + $this->object = new Variable($this->assetRepo); } /** @@ -61,7 +69,7 @@ class VariableTest extends \PHPUnit_Framework_TestCase public function convertVariableNotationDataProvider() { return [ - ['{{base_url_path}}/file.ext', 'http://example.com/pub/static/frontend/Magento/blank/en_US/file.ext'], + ['{{base_url_path}}/file.ext', '{{base_url_path}}frontend/Magento/blank/{{locale}}/file.ext'], ]; } } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Asset/RepositoryTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Asset/RepositoryTest.php index e3c50843976588224b20a9b5236858cae1e2d4c1..3b51f7cc3df2a278561f5007f6380e235226a750 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Asset/RepositoryTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Asset/RepositoryTest.php @@ -6,7 +6,9 @@ namespace Magento\Framework\View\Test\Unit\Asset; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Asset\Repository; +use Magento\Framework\View\Design\Theme\ThemeProviderInterface; /** * Unit test for Magento\Framework\View\Asset\Repository @@ -31,9 +33,9 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase private $designMock; /** - * @var \Magento\Framework\View\Design\Theme\ListInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ThemeProviderInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $listMock; + private $themeProvider; /** * @var \Magento\Framework\View\Asset\Source|\PHPUnit_Framework_MockObject_MockObject @@ -76,9 +78,7 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase $this->designMock = $this->getMockBuilder(\Magento\Framework\View\DesignInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->listMock = $this->getMockBuilder(\Magento\Framework\View\Design\Theme\ListInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $this->themeProvider = $this->getMock(ThemeProviderInterface::class); $this->sourceMock = $this->getMockBuilder(\Magento\Framework\View\Asset\Source::class) ->disableOriginalConstructor() ->getMock(); @@ -103,17 +103,17 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase ->disableOriginalConstructor() ->getMock(); - $this->repository = new Repository( - $this->urlMock, - $this->designMock, - $this->listMock, - $this->sourceMock, - $this->httpMock, - $this->fileFactoryMock, - $this->fallbackFactoryMock, - $this->contextFactoryMock, - $this->remoteFactoryMock - ); + $this->repository = (new ObjectManager($this))->getObject(Repository::class, [ + 'baseUrl' => $this->urlMock, + 'design' => $this->designMock, + 'themeProvider' => $this->themeProvider, + 'assetSource' => $this->sourceMock, + 'request' => $this->httpMock, + 'fileFactory' => $this->fileFactoryMock, + 'fallbackContextFactory' => $this->fallbackFactoryMock, + 'contextFactory' => $this->contextFactoryMock, + 'remoteFactory' => $this->remoteFactoryMock + ]); } /** @@ -124,7 +124,7 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase public function testUpdateDesignParamsWrongTheme() { $params = ['area' => 'area', 'theme' => 'nonexistent_theme']; - $this->listMock->expects($this->once()) + $this->themeProvider->expects($this->once()) ->method('getThemeByFullPath') ->with('area/nonexistent_theme') ->will($this->returnValue(null)); @@ -139,7 +139,7 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase */ public function testUpdateDesignParams($params, $result) { - $this->listMock + $this->themeProvider ->expects($this->any()) ->method('getThemeByFullPath') ->willReturn('ThemeID'); @@ -169,7 +169,7 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase */ public function testCreateAsset() { - $this->listMock + $this->themeProvider ->expects($this->any()) ->method('getThemeByFullPath') ->willReturnArgument(0); @@ -231,7 +231,7 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase 'locale' => 'locale' ] ); - $this->listMock + $this->themeProvider ->expects($this->any()) ->method('getThemeByFullPath') ->willReturnArgument(0); diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Asset/SourceTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Asset/SourceTest.php index 66e33e4fe31401c4f874822551acfb85afb36b01..2a704650aa9dd6618f3925d197d4b9e1c732ade6 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Asset/SourceTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Asset/SourceTest.php @@ -10,9 +10,11 @@ namespace Magento\Framework\View\Test\Unit\Asset; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem\DriverPool; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Asset\PreProcessor\ChainFactoryInterface; use Magento\Framework\View\Asset\PreProcessor\Chain; use Magento\Framework\View\Asset\Source; +use Magento\Framework\View\Design\Theme\ThemeProviderInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -76,16 +78,16 @@ class SourceTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->preProcessorPool = $this->getMock( + $this->preProcessorPool = $this->getMock( \Magento\Framework\View\Asset\PreProcessor\Pool::class, [], [], '', false ); - $this->viewFileResolution = $this->getMock( + $this->viewFileResolution = $this->getMock( \Magento\Framework\View\Design\FileResolution\Fallback\StaticFile::class, [], [], '', false ); $this->theme = $this->getMockForAbstractClass(\Magento\Framework\View\Design\ThemeInterface::class); /** @var \Magento\Framework\App\Config\ScopeConfigInterface $config */ - $this->chainFactory = $this->getMockBuilder( + $this->chainFactory = $this->getMockBuilder( \Magento\Framework\View\Asset\PreProcessor\ChainFactoryInterface::class) ->getMock(); $this->chain = $this->getMockBuilder(\Magento\Framework\View\Asset\PreProcessor\Chain::class) @@ -96,30 +98,30 @@ class SourceTest extends \PHPUnit_Framework_TestCase ->method('create') ->willReturn($this->chain); - $themeList = $this->getMockForAbstractClass(\Magento\Framework\View\Design\Theme\ListInterface::class); - $themeList->expects($this->any()) + $themeProvider = $this->getMock(ThemeProviderInterface::class); + $themeProvider->expects($this->any()) ->method('getThemeByFullPath') ->with('frontend/magento_theme') ->willReturn($this->theme); - $this->readFactory = $this->getMock( - \Magento\Framework\Filesystem\Directory\ReadFactory::class, - [], - [], - '', - false + $this->readFactory = $this->getMock( + \Magento\Framework\Filesystem\Directory\ReadFactory::class, + [], + [], + '', + false ); $this->initFilesystem(); - $this->object = new Source( - $this->filesystem, - $this->readFactory, - $this->preProcessorPool, - $this->viewFileResolution, - $themeList, - $this->chainFactory - ); + $this->object = (new ObjectManager($this))->getObject(Source::class, [ + 'filesystem' => $this->filesystem, + 'readFactory' => $this->readFactory, + 'preProcessorPool' => $this->preProcessorPool, + 'fallback' => $this->viewFileResolution, + 'themeProvider' => $themeProvider, + 'chainFactory' => $this->chainFactory + ]); } /** @@ -229,11 +231,11 @@ class SourceTest extends \PHPUnit_Framework_TestCase protected function initFilesystem() { $this->filesystem = $this->getMock(\Magento\Framework\Filesystem::class, [], [], '', false); - $this->rootDirRead = $this->getMockForAbstractClass( - \Magento\Framework\Filesystem\Directory\ReadInterface::class + $this->rootDirRead = $this->getMockForAbstractClass( + \Magento\Framework\Filesystem\Directory\ReadInterface::class ); - $this->staticDirRead = $this->getMockForAbstractClass( - \Magento\Framework\Filesystem\Directory\ReadInterface::class + $this->staticDirRead = $this->getMockForAbstractClass( + \Magento\Framework\Filesystem\Directory\ReadInterface::class ); $this->varDir = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\Directory\WriteInterface::class); diff --git a/setup/pub/magento/setup/updater-success.js b/setup/pub/magento/setup/updater-success.js index d2e98394251c8cc5233e2852a25fbc71b00890a3..36e58958ed6911949ad0852b9f3b1d87f57ce8ca 100644 --- a/setup/pub/magento/setup/updater-success.js +++ b/setup/pub/magento/setup/updater-success.js @@ -8,8 +8,16 @@ angular.module('updater-success', ['ngStorage']) .controller('updaterSuccessController', ['$scope', '$state', '$localStorage', '$window', 'navigationService', function ($scope, $state, $localStorage, $window, navigationService) { if ($localStorage.successPageAction) { $scope.successPageAction = $localStorage.successPageAction; - $scope.successPageActionMessage = $scope.successPageAction + - ($scope.endsWith($scope.successPageAction, 'e') ? 'd' : 'ed'); + switch (true) { + case $scope.endsWith($scope.successPageAction, 'd'): + $scope.successPageActionMessage = $scope.successPageAction; + break; + case $scope.endsWith($scope.successPageAction, 'e'): + $scope.successPageActionMessage = $scope.successPageAction + 'd'; + break; + default: + $scope.successPageActionMessage = $scope.successPageAction + 'ed'; + } } if ($localStorage.packages) { $scope.packages = $localStorage.packages;