diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml index 7297f3a6c22991215efa0b07ab37087ff7dd9bf9..0eb01d6a252ea20107b04aae6250cbd31ab979c3 100644 --- a/app/code/Magento/Backend/etc/adminhtml/system.xml +++ b/app/code/Magento/Backend/etc/adminhtml/system.xml @@ -198,7 +198,7 @@ <resource>Magento_Config::config_general</resource> <group id="country" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Country Options</label> - <field id="allow" translate="label" type="multiselect" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="allow" translate="label" type="multiselect" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Allow Countries</label> <source_model>Magento\Directory\Model\Config\Source\Country</source_model> <can_be_empty>1</can_be_empty> 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/Catalog/view/frontend/templates/product/widget/link/link_inline.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/link/link_inline.phtml index 262bc431e158efc4abd8da807744bcbdce34b986..386400d7b901cd0bdabda3f706e65f8358547058 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/link/link_inline.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/link/link_inline.phtml @@ -6,6 +6,6 @@ // @codingStandardsIgnoreFile ?> -<div class="widget block block-product-link-inline"> +<span class="widget block block-product-link-inline"> <a <?php /* @escapeNotVerified */ echo $block->getLinkAttributes() ?>><span><?php echo $block->escapeHtml($block->getLabel()) ?></span></a> -</div> +</span> diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 21c12bf0a8b1a64b34db70eaeedd4a78a9682976..75ec58ff908762ff82f3c93e6fa218abde6ba42a 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -1317,7 +1317,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity $select = $this->_connection->select()->from( $entityTable, - $this->getNewSkuFieldsForSelect() + array_merge($this->getNewSkuFieldsForSelect(), $this->getOldSkuFieldsForSelect()) )->where( 'sku IN (?)', array_keys($entityRowsIn) @@ -1330,10 +1330,45 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity $this->skuProcessor->setNewSkuData($sku, $key, $value); } } + + $this->updateOldSku($newProducts); } + return $this; } + /** + * Return additional data, needed to select. + * @return array + */ + private function getOldSkuFieldsForSelect() + { + return ['type_id', 'attribute_set_id']; + } + + /** + * Adds newly created products to _oldSku + * @param array $newProducts + * @return void + */ + private function updateOldSku(array $newProducts) + { + $oldSkus = []; + foreach ($newProducts as $info) { + $typeId = $info['type_id']; + $sku = $info['sku']; + $oldSkus[$sku] = [ + 'type_id' => $typeId, + 'attr_set_id' => $info['attribute_set_id'], + $this->getProductIdentifierField() => $info[$this->getProductIdentifierField()], + 'supported_type' => isset($this->_productTypeModels[$typeId]), + $this->getProductEntityLinkField() => $info[$this->getProductEntityLinkField()], + ]; + } + + $this->_oldSku = array_replace($this->_oldSku, $oldSkus); + } + /** * Get new SKU fields for select * @@ -1718,6 +1753,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity ['adapter' => $this, 'bunch' => $bunch] ); } + return $this; } @@ -2452,6 +2488,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity { $source = $this->_getSource(); $source->rewind(); + while ($source->valid()) { try { $rowData = $source->current(); @@ -2465,6 +2502,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity $rowData = $this->_customFieldsMapping($rowData); $this->validateRow($rowData, $source->key()); + $source->next(); } $this->checkUrlKeyDuplicates(); diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php index a28f65aef1ece1feda826ae9a1a37635af59aae9..8461e617431d64fe3529162d54841d7da4740cdf 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php @@ -567,4 +567,13 @@ abstract class AbstractType } return $this->productEntityLinkField; } + + /** + * Clean cached values + */ + public function __destruct() + { + self::$attributeCodeToId = []; + self::$commonAttributesCache = []; + } } diff --git a/app/code/Magento/Cms/view/frontend/templates/widget/link/link_inline.phtml b/app/code/Magento/Cms/view/frontend/templates/widget/link/link_inline.phtml index 68aafe5973d0ad3fddbb74b9765236d7325dd3e1..382b492db3da5c815530d87f13ee959d82e2c083 100644 --- a/app/code/Magento/Cms/view/frontend/templates/widget/link/link_inline.phtml +++ b/app/code/Magento/Cms/view/frontend/templates/widget/link/link_inline.phtml @@ -4,8 +4,8 @@ * See COPYING.txt for license details. */ ?> -<div class="widget block block-cms-link-inline"> +<span class="widget block block-cms-link-inline"> <a <?php /* @escapeNotVerified */ echo $block->getLinkAttributes() ?>> <span><?php echo $block->escapeHtml($block->getLabel()) ?></span> </a> -</div> +</span> diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php b/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php index 7ba330f5a060c615b74bcb6cba86e5116e36f767..321870b96d32abf13c48fdc0975efc9ba5248f17 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php @@ -148,10 +148,14 @@ class VariationHandler \Magento\Catalog\Model\Product $parentProduct, $postData ) { + $typeId = isset($postData['weight']) && !empty($postData['weight']) + ? ProductType::TYPE_SIMPLE + : ProductType::TYPE_VIRTUAL; + $product->setStoreId( \Magento\Store\Model\Store::DEFAULT_STORE_ID )->setTypeId( - $postData['weight'] ? ProductType::TYPE_SIMPLE : ProductType::TYPE_VIRTUAL + $typeId )->setAttributeSetId( $parentProduct->getNewVariationsAttributeSetId() ); diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Render/FinalPriceBox.php b/app/code/Magento/ConfigurableProduct/Pricing/Render/FinalPriceBox.php new file mode 100644 index 0000000000000000000000000000000000000000..16a296a3554548d31f1692f397cd0124e272a1af --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Pricing/Render/FinalPriceBox.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\ConfigurableProduct\Pricing\Render; + +use Magento\Catalog\Pricing\Price\FinalPrice; +use Magento\Catalog\Pricing\Price\RegularPrice; +use Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterface; +use Magento\Framework\Pricing\Price\PriceInterface; +use Magento\Framework\Pricing\Render\RendererPool; +use Magento\Framework\Pricing\SaleableInterface; +use Magento\Framework\View\Element\Template\Context; + +class FinalPriceBox extends \Magento\Catalog\Pricing\Render\FinalPriceBox +{ + /** + * @var ConfigurableOptionsProviderInterface + */ + private $configurableOptionsProvider; + + /** + * @param Context $context + * @param SaleableInterface $saleableItem + * @param PriceInterface $price + * @param RendererPool $rendererPool + * @param ConfigurableOptionsProviderInterface $configurableOptionsProvider + * @param array $data + */ + public function __construct( + Context $context, + SaleableInterface $saleableItem, + PriceInterface $price, + RendererPool $rendererPool, + ConfigurableOptionsProviderInterface $configurableOptionsProvider, + array $data = [] + ) { + $this->configurableOptionsProvider = $configurableOptionsProvider; + parent::__construct($context, $saleableItem, $price, $rendererPool, $data); + } + + /** + * Define if the special price should be shown + * + * @return bool + */ + public function hasSpecialPrice() + { + $product = $this->getSaleableItem(); + foreach ($this->configurableOptionsProvider->getProducts($product) as $subProduct) { + $regularPrice = $subProduct->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE)->getValue(); + $finalPrice = $subProduct->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE)->getValue(); + if ($finalPrice < $regularPrice) { + return true; + } + } + return false; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/VariationHandlerTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/VariationHandlerTest.php index fb991879bbca391abc170e55c061762ab6aa4f2c..62f5a8089e350d247a75595d24214a08745acf47 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/VariationHandlerTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/VariationHandlerTest.php @@ -8,6 +8,7 @@ namespace Magento\ConfigurableProduct\Test\Unit\Model\Product; +use Magento\Catalog\Model\Product\Type; use Magento\ConfigurableProduct\Model\Product\VariationHandler; /** @@ -162,23 +163,30 @@ class VariationHandlerTest extends \PHPUnit_Framework_TestCase /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @dataProvider dataProviderTestGenerateSimpleProducts + * @param int|string|null $weight + * @param string $typeId */ - public function testGenerateSimpleProducts() + public function testGenerateSimpleProducts($weight, $typeId) { $productsData = [ - 6 => - [ - 'image' => 'image.jpg', - 'name' => 'config-red', - 'configurable_attribute' => '{"new_attr":"6"}', - 'sku' => 'config-red', - 'quantity_and_stock_status' => - [ - 'qty' => '', - ], - 'weight' => '333', - ] + [ + 'image' => 'image.jpg', + 'name' => 'config-red', + 'configurable_attribute' => '{"new_attr":"6"}', + 'sku' => 'config-red', + 'quantity_and_stock_status' => + [ + 'qty' => '', + ], + ] ]; + + // Do not add 'weight' attribute if it's value is null! + if ($weight !== null) { + $productsData[0]['weight'] = $weight; + } + $stockData = [ 'manage_stock' => '0', 'use_config_enable_qty_increments' => '1', @@ -218,7 +226,7 @@ class VariationHandlerTest extends \PHPUnit_Framework_TestCase ) ->disableOriginalConstructor() ->getMock(); - $productTypeMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Type::class) + $productTypeMock = $this->getMockBuilder(Type::class) ->setMethods(['getSetAttributes']) ->disableOriginalConstructor() ->getMock(); @@ -236,7 +244,7 @@ class VariationHandlerTest extends \PHPUnit_Framework_TestCase ->willReturn('new_attr_set_id'); $this->productFactoryMock->expects($this->once())->method('create')->willReturn($newSimpleProductMock); $newSimpleProductMock->expects($this->once())->method('setStoreId')->with(0)->willReturnSelf(); - $newSimpleProductMock->expects($this->once())->method('setTypeId')->with('simple')->willReturnSelf(); + $newSimpleProductMock->expects($this->once())->method('setTypeId')->with($typeId)->willReturnSelf(); $newSimpleProductMock->expects($this->once()) ->method('setAttributeSetId') ->with('new_attr_set_id') @@ -265,6 +273,27 @@ class VariationHandlerTest extends \PHPUnit_Framework_TestCase $this->assertEquals(['product_id'], $this->model->generateSimpleProducts($parentProductMock, $productsData)); } + /** + * @return array + */ + public function dataProviderTestGenerateSimpleProducts() + { + return [ + [ + 'weight' => 333, + 'type_id' => Type::TYPE_SIMPLE, + ], + [ + 'weight' => '', + 'type_id' => Type::TYPE_VIRTUAL, + ], + [ + 'weight' => null, + 'type_id' => Type::TYPE_VIRTUAL, + ], + ]; + } + public function testProcessMediaGalleryWithImagesAndGallery() { $this->product->expects($this->atLeastOnce())->method('getMediaGallery')->with('images')->willReturn([]); diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Render/FinalPriceBoxTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Render/FinalPriceBoxTest.php new file mode 100644 index 0000000000000000000000000000000000000000..4dbcfed5315252c62494b6ede30d4d14f9a598b2 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Render/FinalPriceBoxTest.php @@ -0,0 +1,137 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\ConfigurableProduct\Test\Unit\Pricing\Render; + +use Magento\Catalog\Pricing\Price\FinalPrice; +use Magento\Catalog\Pricing\Price\RegularPrice; +use Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterface; +use Magento\ConfigurableProduct\Pricing\Render\FinalPriceBox; + +class FinalPriceBoxTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Framework\View\Element\Template\Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject + */ + private $saleableItem; + + /** + * @var \Magento\Framework\Pricing\Price\PriceInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $price; + + /** + * @var \Magento\Framework\Pricing\Render\RendererPool|\PHPUnit_Framework_MockObject_MockObject + */ + private $rendererPool; + + /** + * @var ConfigurableOptionsProviderInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configurableOptionsProvider; + + /** + * @var FinalPriceBox + */ + private $model; + + protected function setUp() + { + $this->context = $this->getMockBuilder(\Magento\Framework\View\Element\Template\Context::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->saleableItem = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->price = $this->getMockBuilder(\Magento\Framework\Pricing\Price\PriceInterface::class) + ->getMockForAbstractClass(); + + $this->rendererPool = $this->getMockBuilder(\Magento\Framework\Pricing\Render\RendererPool::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->configurableOptionsProvider = $this->getMockBuilder(ConfigurableOptionsProviderInterface::class) + ->getMockForAbstractClass(); + + $this->model = new FinalPriceBox( + $this->context, + $this->saleableItem, + $this->price, + $this->rendererPool, + $this->configurableOptionsProvider + ); + } + + /** + * @param float $regularPrice + * @param float $finalPrice + * @param bool $expected + * @dataProvider hasSpecialPriceDataProvider + */ + public function testHasSpecialPrice( + $regularPrice, + $finalPrice, + $expected + ) { + $priceMockOne = $this->getMockBuilder(\Magento\Framework\Pricing\Price\PriceInterface::class) + ->getMockForAbstractClass(); + + $priceMockOne->expects($this->once()) + ->method('getValue') + ->willReturn($regularPrice); + + $priceMockTwo = $this->getMockBuilder(\Magento\Framework\Pricing\Price\PriceInterface::class) + ->getMockForAbstractClass(); + + $priceMockTwo->expects($this->once()) + ->method('getValue') + ->willReturn($finalPrice); + + $priceInfoMock = $this->getMockBuilder(\Magento\Framework\Pricing\PriceInfo\Base::class) + ->disableOriginalConstructor() + ->getMock(); + + $priceInfoMock->expects($this->exactly(2)) + ->method('getPrice') + ->willReturnMap([ + [RegularPrice::PRICE_CODE, $priceMockOne], + [FinalPrice::PRICE_CODE, $priceMockTwo], + ]); + + $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) + ->setMethods(['getPriceInfo']) + ->getMockForAbstractClass(); + + $productMock->expects($this->exactly(2)) + ->method('getPriceInfo') + ->willReturn($priceInfoMock); + + $this->configurableOptionsProvider->expects($this->once()) + ->method('getProducts') + ->with($this->saleableItem) + ->willReturn([$productMock]); + + $this->assertEquals($expected, $this->model->hasSpecialPrice()); + } + + /** + * @return array + */ + public function hasSpecialPriceDataProvider() + { + return [ + [10., 20., false], + [10., 10., false], + [20., 10., true], + ]; + } +} diff --git a/app/code/Magento/ConfigurableProduct/view/base/layout/catalog_product_prices.xml b/app/code/Magento/ConfigurableProduct/view/base/layout/catalog_product_prices.xml new file mode 100644 index 0000000000000000000000000000000000000000..47fe31681b5bf0d96ba4cf4935283d415de23ce7 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/view/base/layout/catalog_product_prices.xml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> + <referenceBlock name="render.product.prices"> + <arguments> + <argument name="configurable" xsi:type="array"> + <item name="prices" xsi:type="array"> + <item name="final_price" xsi:type="array"> + <item name="render_class" xsi:type="string">Magento\ConfigurableProduct\Pricing\Render\FinalPriceBox</item> + <item name="render_template" xsi:type="string">Magento_ConfigurableProduct::product/price/final_price.phtml</item> + </item> + </item> + </argument> + </arguments> + </referenceBlock> +</layout> diff --git a/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/final_price.phtml b/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/final_price.phtml new file mode 100644 index 0000000000000000000000000000000000000000..5943b1ea2af5b5c4a9095a81b3a22dc1019f1da9 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/final_price.phtml @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +// @codingStandardsIgnoreFile + +?> + +<?php +/** @var \Magento\ConfigurableProduct\Pricing\Render\FinalPriceBox$block */ + +/** @var \Magento\Framework\Pricing\Price\PriceInterface $priceModel */ +$priceModel = $block->getPriceType('regular_price'); + +/** @var \Magento\Framework\Pricing\Price\PriceInterface $finalPriceModel */ +$finalPriceModel = $block->getPriceType('final_price'); +$idSuffix = $block->getIdSuffix() ? $block->getIdSuffix() : ''; +$schema = ($block->getZone() == 'item_view') ? true : false; +?> +<?php if ($block->hasSpecialPrice()): ?> + <span class="special-price"> + <?php /* @escapeNotVerified */ echo $block->renderAmount($finalPriceModel->getAmount(), [ + 'display_label' => __('Special Price'), + 'price_id' => $block->getPriceId('product-price-' . $idSuffix), + 'price_type' => 'finalPrice', + 'include_container' => true, + 'schema' => $schema + ]); ?> + </span> + <span class="old-price sly-old-price no-display"> + <?php /* @escapeNotVerified */ echo $block->renderAmount($priceModel->getAmount(), [ + 'display_label' => __('Regular Price'), + 'price_id' => $block->getPriceId('old-price-' . $idSuffix), + 'price_type' => 'oldPrice', + 'include_container' => true, + 'skip_adjustments' => true + ]); ?> + </span> +<?php else: ?> + <?php /* @escapeNotVerified */ echo $block->renderAmount($finalPriceModel->getAmount(), [ + 'price_id' => $block->getPriceId('product-price-' . $idSuffix), + 'price_type' => 'finalPrice', + 'include_container' => true, + 'schema' => $schema + ]); ?> +<?php endif; ?> + +<?php if ($block->showMinimalPrice()): ?> + <?php if ($block->getUseLinkForAsLowAs()):?> + <a href="<?php /* @escapeNotVerified */ echo $block->getSaleableItem()->getProductUrl(); ?>" class="minimal-price-link"> + <?php /* @escapeNotVerified */ echo $block->renderAmountMinimal(); ?> + </a> + <?php else:?> + <span class="minimal-price-link"> + <?php /* @escapeNotVerified */ echo $block->renderAmountMinimal(); ?> + </span> + <?php endif?> +<?php endif; ?> diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js index bd0314aacf71106c11fa3759a48a9fde13967826..7bea20e78620134fff06d4cd259fee029f4fe637 100644 --- a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js @@ -28,6 +28,7 @@ define([ '<% } %>', mediaGallerySelector: '[data-gallery-role=gallery-placeholder]', mediaGalleryInitial: null, + slyOldPriceSelector: '.sly-old-price', onlyMainImg: false }, @@ -248,6 +249,7 @@ define([ this._resetChildren(element); } this._reloadPrice(); + this._displayRegularPriceBlock(this.simpleProduct); this._changeProductImage(); }, @@ -444,7 +446,7 @@ define([ }, /** - * Returns pracies for configured products + * Returns prices for configured products * * @param {*} config - Products configuration * @returns {*} @@ -487,6 +489,23 @@ define([ undefined : _.first(config.allowedProducts); + }, + + /** + * Show or hide regular price block + * + * @param {*} optionId + * @private + */ + _displayRegularPriceBlock: function (optionId) { + if (typeof optionId != 'undefined' + && this.options.spConfig.optionPrices[optionId].oldPrice.amount + != this.options.spConfig.optionPrices[optionId].finalPrice.amount + ) { + $(this.options.slyOldPriceSelector).show(); + } else { + $(this.options.slyOldPriceSelector).hide(); + } } }); diff --git a/app/code/Magento/Customer/Model/Customer/DataProvider.php b/app/code/Magento/Customer/Model/Customer/DataProvider.php index 2c1f3bdb2388c8cca9b227c9fdb0131fb8f008ec..2f46459a794eca284e91c1dddf3c2e39def0cd9f 100644 --- a/app/code/Magento/Customer/Model/Customer/DataProvider.php +++ b/app/code/Magento/Customer/Model/Customer/DataProvider.php @@ -7,9 +7,12 @@ namespace Magento\Customer\Model\Customer; use Magento\Customer\Api\AddressMetadataInterface; use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\Attribute; use Magento\Customer\Model\FileProcessor; use Magento\Customer\Model\FileProcessorFactory; +use Magento\Customer\Model\ResourceModel\Address\Attribute\Source\CountryWithWebsites; use Magento\Eav\Api\Data\AttributeInterface; use Magento\Eav\Model\Config; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; @@ -55,6 +58,16 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider */ protected $loadedData; + /** + * @var CountryWithWebsites + */ + private $countryWithWebsiteSource; + + /** + * @var \Magento\Customer\Model\Config\Share + */ + private $shareConfig; + /** * EAV attribute properties to fetch from meta storage * @var array @@ -117,6 +130,7 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider * @param FileProcessorFactory $fileProcessorFactory * @param array $meta * @param array $data + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( $name, @@ -234,6 +248,7 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider * @param Attribute $attribute * @param array $customerData * @return array + * @SuppressWarnings(PHPMD.NPathComplexity) */ private function getFileUploaderData( Type $entityType, @@ -292,6 +307,7 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider $this->processFrontendInput($attribute, $meta); $code = $attribute->getAttributeCode(); + // use getDataUsingMethod, since some getters are defined and apply additional processing of returning value foreach ($this->metaProperties as $metaName => $origName) { $value = $attribute->getDataUsingMethod($origName); @@ -304,7 +320,12 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider } if ($attribute->usesSource()) { - $meta[$code]['arguments']['data']['config']['options'] = $attribute->getSource()->getAllOptions(); + if ($code == AddressInterface::COUNTRY_ID) { + $meta[$code]['arguments']['data']['config']['options'] = $this->getCountryWithWebsiteSource() + ->getAllOptions(); + } else { + $meta[$code]['arguments']['data']['config']['options'] = $attribute->getSource()->getAllOptions(); + } } $rules = $this->eavValidationRules->build($attribute, $meta[$code]['arguments']['data']['config']); @@ -315,9 +336,61 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider $this->overrideFileUploaderMetadata($entityType, $attribute, $meta[$code]['arguments']['data']['config']); } + + $this->processWebsiteMeta($meta); return $meta; } + /** + * Retrieve Country With Websites Source + * + * @deprecated + * @return CountryWithWebsites + */ + private function getCountryWithWebsiteSource() + { + if (!$this->countryWithWebsiteSource) { + $this->countryWithWebsiteSource = ObjectManager::getInstance()->get(CountryWithWebsites::class); + } + + return $this->countryWithWebsiteSource; + } + + /** + * Retrieve Customer Config Share + * + * @deprecated + * @return \Magento\Customer\Model\Config\Share + */ + private function getShareConfig() + { + if (!$this->shareConfig) { + $this->shareConfig = ObjectManager::getInstance()->get(\Magento\Customer\Model\Config\Share::class); + } + + return $this->shareConfig; + } + + /** + * Add global scope parameter and filter options to website meta + * + * @param array $meta + * @return void + */ + private function processWebsiteMeta(&$meta) + { + if (isset($meta[CustomerInterface::WEBSITE_ID]) && $this->getShareConfig()->isGlobalScope()) { + $meta[CustomerInterface::WEBSITE_ID]['arguments']['data']['config']['isGlobalScope'] = 1; + } + + if (isset($meta[AddressInterface::COUNTRY_ID]) && !$this->getShareConfig()->isGlobalScope()) { + $meta[AddressInterface::COUNTRY_ID]['arguments']['data']['config']['filterBy'] = [ + 'target' => '${ $.provider }:data.customer.website_id', + 'field' => 'website_ids' + ]; + } + } + /** * Override file uploader UI component metadata * diff --git a/app/code/Magento/Customer/Model/Plugin/AllowedCountries.php b/app/code/Magento/Customer/Model/Plugin/AllowedCountries.php new file mode 100644 index 0000000000000000000000000000000000000000..157643bdfe5cc070b24b6ed4fe7ce19c6458ba37 --- /dev/null +++ b/app/code/Magento/Customer/Model/Plugin/AllowedCountries.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Customer\Model\Plugin; + +use Magento\Customer\Model\Config\Share; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Class AllowedCountries + */ +class AllowedCountries +{ + /** + * @var \Magento\Customer\Model\Config\Share + */ + private $shareConfig; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param Share $share + * @param StoreManagerInterface $storeManager + */ + public function __construct( + Share $share, + StoreManagerInterface $storeManager + ) { + $this->shareConfig = $share; + $this->storeManager = $storeManager; + } + + /** + * Retrieve all allowed countries or specific by scope depends on customer share setting + * + * @param \Magento\Directory\Model\AllowedCountries $subject + * @param string | null $filter + * @param string $scope + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeGetAllowedCountries( + \Magento\Directory\Model\AllowedCountries $subject, + $scope = ScopeInterface::SCOPE_WEBSITE, + $scopeCode = null + ) { + if ($this->shareConfig->isGlobalScope()) { + //Check if we have shared accounts - than merge all website allowed countries + $scopeCode = array_map(function (WebsiteInterface $website) { + return $website->getId(); + }, $this->storeManager->getWebsites()); + $scope = ScopeInterface::SCOPE_WEBSITES; + } + + return [$scope, $scopeCode]; + } +} diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Country.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Country.php index 72a18afa07a27ae7653c849fbbbc44c55badacb0..13d23d91880aac2ba0c122e88a1e02352f3f9137 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Country.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Country.php @@ -11,6 +11,15 @@ */ namespace Magento\Customer\Model\ResourceModel\Address\Attribute\Source; +use Magento\Checkout\Model\Session; +use Magento\Framework\App\ObjectManager; +use Magento\Store\Api\StoreResolverInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Class Country. + * @package Magento\Customer\Model\ResourceModel\Address\Attribute\Source + */ class Country extends \Magento\Eav\Model\Entity\Attribute\Source\Table { /** @@ -18,6 +27,11 @@ class Country extends \Magento\Eav\Model\Entity\Attribute\Source\Table */ protected $_countriesFactory; + /** + * @var StoreResolverInterface + */ + private $storeResolver; + /** * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\CollectionFactory $attrOptionCollectionFactory * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\OptionFactory $attrOptionFactory @@ -41,7 +55,7 @@ class Country extends \Magento\Eav\Model\Entity\Attribute\Source\Table { if (!$this->_options) { $this->_options = $this->_createCountriesCollection()->loadByStore( - $this->getAttribute()->getStoreId() + $this->getStoreResolver()->getCurrentStoreId() )->toOptionArray(); } return $this->_options; @@ -54,4 +68,18 @@ class Country extends \Magento\Eav\Model\Entity\Attribute\Source\Table { return $this->_countriesFactory->create(); } + + /** + * Retrieve Store Resolver + * @deprecated + * @return StoreResolverInterface + */ + private function getStoreResolver() + { + if (!$this->storeResolver) { + $this->storeResolver = ObjectManager::getInstance()->get(StoreResolverInterface::class); + } + + return $this->storeResolver; + } } diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php new file mode 100644 index 0000000000000000000000000000000000000000..16ceee8a3f28dd91145915cefefaab842ab50c6e --- /dev/null +++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * Customer country with website specified attribute source + * + * @author Magento Core Team <core@magentocommerce.com> + */ +namespace Magento\Customer\Model\ResourceModel\Address\Attribute\Source; + +use Magento\Customer\Model\Config\Share; +use Magento\Directory\Model\AllowedCountries; +use Magento\Store\Model\ScopeInterface; + +class CountryWithWebsites extends \Magento\Eav\Model\Entity\Attribute\Source\Table +{ + /** + * @var \Magento\Directory\Model\ResourceModel\Country\CollectionFactory + */ + private $countriesFactory; + + /** + * @var \Magento\Directory\Model\AllowedCountries + */ + private $allowedCountriesReader; + + /** + * @var array + */ + private $options; + + /** + * @var \Magento\Store\Model\StoreManagerInterface + */ + private $storeManager; + + /** + * @var Share + */ + private $shareConfig; + + /** + * CountryWithWebsites constructor. + * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\CollectionFactory $attrOptionCollectionFactory + * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\OptionFactory $attrOptionFactory + * @param \Magento\Directory\Model\ResourceModel\Country\CollectionFactory $countriesFactory + * @param AllowedCountries $allowedCountriesReader + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param Share $shareConfig + */ + public function __construct( + \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\CollectionFactory $attrOptionCollectionFactory, + \Magento\Eav\Model\ResourceModel\Entity\Attribute\OptionFactory $attrOptionFactory, + \Magento\Directory\Model\ResourceModel\Country\CollectionFactory $countriesFactory, + \Magento\Directory\Model\AllowedCountries $allowedCountriesReader, + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Magento\Customer\Model\Config\Share $shareConfig + ) { + $this->countriesFactory = $countriesFactory; + $this->allowedCountriesReader = $allowedCountriesReader; + $this->storeManager = $storeManager; + $this->shareConfig = $shareConfig; + parent::__construct($attrOptionCollectionFactory, $attrOptionFactory); + } + + /** + * Retrieve all options + * + * @return array + */ + public function getAllOptions() + { + if (!$this->options) { + $allowedCountries = []; + $websiteIds = []; + + if (!$this->shareConfig->isGlobalScope()) { + foreach ($this->storeManager->getWebsites() as $website) { + $countries = $this->allowedCountriesReader + ->getAllowedCountries(ScopeInterface::SCOPE_WEBSITE, $website->getId()); + $allowedCountries = array_merge($allowedCountries, $countries); + + foreach ($countries as $countryCode) { + $websiteIds[$countryCode][] = $website->getId(); + } + } + } else { + $allowedCountries = $this->allowedCountriesReader->getAllowedCountries(); + } + + $this->options = $this->createCountriesCollection() + ->addFieldToFilter('country_id', ['in' => $allowedCountries]) + ->toOptionArray(); + + foreach ($this->options as &$option) { + if (isset($websiteIds[$option['value']])) { + $option['website_ids'] = $websiteIds[$option['value']]; + } + } + } + + return $this->options; + } + + /** + * Create Countries Collection with all countries + * + * @return \Magento\Directory\Model\ResourceModel\Country\Collection + */ + private function createCountriesCollection() + { + return $this->countriesFactory->create(); + } +} diff --git a/app/code/Magento/Customer/Setup/UpgradeData.php b/app/code/Magento/Customer/Setup/UpgradeData.php index 4c4452f1a4c81d1ec4e2f8031518cf87049f7c35..c03a1dec02d80e2ff01dc83ac2fa6a5ff156552c 100644 --- a/app/code/Magento/Customer/Setup/UpgradeData.php +++ b/app/code/Magento/Customer/Setup/UpgradeData.php @@ -7,11 +7,16 @@ namespace Magento\Customer\Setup; use Magento\Customer\Model\Customer; +use Magento\Directory\Model\AllowedCountries; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Encryption\Encryptor; use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Framework\Setup\SetupInterface; use Magento\Framework\Setup\UpgradeDataInterface; use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; /** * @codeCoverageIgnore @@ -26,6 +31,11 @@ class UpgradeData implements UpgradeDataInterface */ protected $customerSetupFactory; + /** + * @var AllowedCountries + */ + private $allowedCountriesReader; + /** * @var IndexerRegistry */ @@ -36,6 +46,11 @@ class UpgradeData implements UpgradeDataInterface */ protected $eavConfig; + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** * @param CustomerSetupFactory $customerSetupFactory * @param IndexerRegistry $indexerRegistry @@ -106,12 +121,140 @@ class UpgradeData implements UpgradeDataInterface $this->upgradeCustomerPasswordResetlinkExpirationPeriodConfig($setup); } + if (version_compare($context->getVersion(), '2.0.9', '<')) { + $setup->getConnection()->beginTransaction(); + + try { + $this->migrateStoresAllowedCountriesToWebsite($setup); + $setup->getConnection()->commit(); + } catch (\Exception $e) { + $setup->getConnection()->rollBack(); + throw $e; + } + } + $indexer = $this->indexerRegistry->get(Customer::CUSTOMER_GRID_INDEXER_ID); $indexer->reindexAll(); $this->eavConfig->clear(); $setup->endSetup(); } + /** + * Retrieve Store Manager + * + * @deprecated + * @return StoreManagerInterface + */ + private function getStoreManager() + { + if (!$this->storeManager) { + $this->storeManager = ObjectManager::getInstance()->get(StoreManagerInterface::class); + } + + return $this->storeManager; + } + + /** + * Retrieve Allowed Countries Reader + * + * @deprecated + * @return AllowedCountries + */ + private function getAllowedCountriesReader() + { + if (!$this->allowedCountriesReader) { + $this->allowedCountriesReader = ObjectManager::getInstance()->get(AllowedCountries::class); + } + + return $this->allowedCountriesReader; + } + + /** + * Merge allowed countries between different scopes + * + * @param array $countries + * @param array $newCountries + * @param string $identifier + * @return array + */ + private function mergeAllowedCountries(array $countries, array $newCountries, $identifier) + { + if (!isset($countries[$identifier])) { + $countries[$identifier] = $newCountries; + } else { + $countries[$identifier] = + array_replace($countries[$identifier], $newCountries); + } + + return $countries; + } + + /** + * Retrieve countries not depending on global scope + * + * @param string $scope + * @param int $scopeCode + * @return array + */ + private function getAllowedCountries($scope, $scopeCode) + { + $reader = $this->getAllowedCountriesReader(); + return $reader->makeCountriesUnique($reader->getCountriesFromConfig($scope, $scopeCode)); + } + + /** + * Merge allowed countries from stores to websites + * + * @param SetupInterface $setup + * @return void + */ + private function migrateStoresAllowedCountriesToWebsite(SetupInterface $setup) + { + $allowedCountries = []; + //Process Websites + foreach ($this->getStoreManager()->getStores() as $store) { + $allowedCountries = $this->mergeAllowedCountries( + $allowedCountries, + $this->getAllowedCountries(ScopeInterface::SCOPE_STORE, $store->getId()), + $store->getWebsiteId() + ); + } + //Process stores + foreach ($this->getStoreManager()->getWebsites() as $website) { + $allowedCountries = $this->mergeAllowedCountries( + $allowedCountries, + $this->getAllowedCountries(ScopeInterface::SCOPE_WEBSITE, $website->getId()), + $website->getId() + ); + } + + $connection = $setup->getConnection(); + + //Remove everything from stores scope + $connection->delete( + $setup->getTable('core_config_data'), + [ + 'path = ?' => AllowedCountries::ALLOWED_COUNTRIES_PATH, + 'scope = ?' => ScopeInterface::SCOPE_STORES + ] + ); + + //Update websites + foreach ($allowedCountries as $scopeId => $countries) { + $connection->update( + $setup->getTable('core_config_data'), + [ + 'value' => implode(',', $countries) + ], + [ + 'path = ?' => AllowedCountries::ALLOWED_COUNTRIES_PATH, + 'scope_id = ?' => $scopeId, + 'scope = ?' => ScopeInterface::SCOPE_WEBSITES + ] + ); + } + } + /** * @param array $entityAttributes * @param CustomerSetup $customerSetup diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php index 5e022dd8feddb252146f43fff55a1c1b716dad49..e594432b5bd66358a2ed13f3d93d41f26f73a7bd 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php @@ -6,6 +6,8 @@ namespace Magento\Customer\Test\Unit\Model\Customer; use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Model\Config\Share; +use Magento\Customer\Model\ResourceModel\Address\Attribute\Source\CountryWithWebsites; use Magento\Eav\Model\Config; use Magento\Eav\Model\Entity\Type; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -103,6 +105,7 @@ class DataProviderTest extends \PHPUnit_Framework_TestCase public function testGetAttributesMetaWithOptions(array $expected) { $helper = new ObjectManager($this); + /** @var \Magento\Customer\Model\Customer\DataProvider $dataProvider */ $dataProvider = $helper->getObject( \Magento\Customer\Model\Customer\DataProvider::class, [ @@ -227,6 +230,29 @@ class DataProviderTest extends \PHPUnit_Framework_TestCase ], ], ], + 'country_id' => [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'dataType' => 'frontend_input', + 'formElement' => 'frontend_input', + 'options' => 'test-options', + 'visible' => 'is_visible', + 'required' => 'is_required', + 'label' => __('frontend_label'), + 'sortOrder' => 'sort_order', + 'notice' => 'note', + 'default' => 'default_value', + 'size' => 'multiline_count', + 'componentType' => Field::NAME, + 'filterBy' => [ + 'target' => '${ $.provider }:data.customer.website_id', + 'field' => 'website_ids' + ] + ], + ], + ], + ] ], ], ] @@ -298,7 +324,7 @@ class DataProviderTest extends \PHPUnit_Framework_TestCase $typeAddressMock->expects($this->once()) ->method('getAttributeCollection') - ->willReturn($this->getAttributeMock()); + ->willReturn($this->getAttributeMock('address')); return $typeAddressMock; } @@ -306,7 +332,7 @@ class DataProviderTest extends \PHPUnit_Framework_TestCase /** * @return AbstractAttribute[]|\PHPUnit_Framework_MockObject_MockObject[] */ - protected function getAttributeMock() + protected function getAttributeMock($type = 'customer') { $attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) ->setMethods(['getAttributeCode', 'getDataUsingMethod', 'usesSource', 'getSource']) @@ -365,10 +391,66 @@ class DataProviderTest extends \PHPUnit_Framework_TestCase [$attributeMock, $this->logicalNot($this->isEmpty()), []], [$attributeBooleanMock, $this->logicalNot($this->isEmpty()), []], ]); + $mocks = [$attributeMock, $attributeBooleanMock]; + + if ($type == "address") { + $mocks[] = $this->getCountryAttrMock(); + } + return $mocks; + } + + private function getCountryAttrMock() + { + $countryByWebsiteMock = $this->getMockBuilder(CountryWithWebsites::class) + ->disableOriginalConstructor() + ->getMock(); + $countryByWebsiteMock->expects($this->any()) + ->method('getAllOptions') + ->willReturn('test-options'); + $shareMock = $this->getMockBuilder(Share::class) + ->disableOriginalConstructor() + ->getMock(); + $objectManagerMock = $this->getMock(\Magento\Framework\ObjectManagerInterface::class); + $objectManagerMock->expects($this->any()) + ->method('get') + ->willReturnMap([ + [CountryWithWebsites::class, $countryByWebsiteMock], + [Share::class, $shareMock], + ]); + \Magento\Framework\App\ObjectManager::setInstance($objectManagerMock); + $countryAttrMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) + ->setMethods(['getAttributeCode', 'getDataUsingMethod', 'usesSource', 'getSource']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $countryAttrMock->expects($this->exactly(2)) + ->method('getAttributeCode') + ->willReturn('country_id'); - return [$attributeMock, $attributeBooleanMock]; + $countryAttrMock->expects($this->any()) + ->method('getDataUsingMethod') + ->willReturnCallback( + function ($origName) { + return $origName; + } + ); + $countryAttrMock->expects($this->any()) + ->method('getLabel') + ->willReturn(__('frontend_label')); + $countryAttrMock->expects($this->any()) + ->method('usesSource') + ->willReturn(true); + $countryAttrMock->expects($this->any()) + ->method('getSource') + ->willReturn(null); + + return $countryAttrMock; } + /** + * @return void + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ public function testGetData() { $customer = $this->getMockBuilder(\Magento\Customer\Model\Customer::class) @@ -478,6 +560,10 @@ class DataProviderTest extends \PHPUnit_Framework_TestCase ); } + /** + * @return void + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ public function testGetDataWithCustomerFormData() { $customerId = 11; @@ -591,6 +677,10 @@ class DataProviderTest extends \PHPUnit_Framework_TestCase $this->assertEquals([$customerId => $customerFormData], $dataProvider->getData()); } + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return void + */ public function testGetDataWithCustomAttributeImage() { $customerId = 1; @@ -812,6 +902,10 @@ class DataProviderTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expectedData, $dataProvider->getData()); } + /** + * @return void + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ public function testGetAttributesMetaWithCustomAttributeImage() { $maxFileSize = 1000; diff --git a/app/code/Magento/Customer/Test/Unit/Model/Plugin/AllowedCountriesTest.php b/app/code/Magento/Customer/Test/Unit/Model/Plugin/AllowedCountriesTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e332d5deca626fb198669feb23011aa0e92a3be1 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Model/Plugin/AllowedCountriesTest.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Test\Unit\Model\Plugin; + +use Magento\Customer\Model\Config\Share; +use Magento\Customer\Model\Plugin\AllowedCountries; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; + +class AllowedCountriesTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Customer\Model\Config\Share | \PHPUnit_Framework_MockObject_MockObject + */ + private $shareConfig; + + /** + * @var StoreManagerInterface | \PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; + + /** @var AllowedCountries */ + private $plugin; + + public function setUp() + { + $this->shareConfig = $this->getMockBuilder(Share::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeManager = $this->getMock(StoreManagerInterface::class); + + $this->plugin = new AllowedCountries($this->shareConfig, $this->storeManager); + } + + public function testGetAllowedCountriesWithGlobalScope() + { + $expectedFilter = 1; + $expectedScope = ScopeInterface::SCOPE_WEBSITES; + + $this->shareConfig->expects($this->once()) + ->method('isGlobalScope') + ->willReturn(true); + $originalAllowedCountriesMock = $this->getMockBuilder(\Magento\Directory\Model\AllowedCountries::class) + ->disableOriginalConstructor() + ->getMock(); + $websiteMock = $this->getMock(WebsiteInterface::class); + $websiteMock->expects($this->once()) + ->method('getId') + ->willReturn($expectedFilter); + $this->storeManager->expects($this->once()) + ->method('getWebsites') + ->willReturn([$websiteMock]); + + $this->assertEquals( + [$expectedScope, [$expectedFilter]], + $this->plugin->beforeGetAllowedCountries($originalAllowedCountriesMock) + ); + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsitesTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsitesTest.php new file mode 100644 index 0000000000000000000000000000000000000000..c91070b139ad6700cee3d032bde087359ff9ad3e --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsitesTest.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Customer\Test\Unit\Model\ResourceModel\Address\Attribute\Source; + +use Magento\Customer\Model\Config\Share; +use Magento\Customer\Model\ResourceModel\Address\Attribute\Source\CountryWithWebsites; +use Magento\Directory\Model\AllowedCountries; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Model\StoreManagerInterface; + +class CountryWithWebsitesTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Directory\Model\ResourceModel\Country\CollectionFactory | \PHPUnit_Framework_MockObject_MockObject + */ + private $countriesFactoryMock; + + /** + * @var \Magento\Directory\Model\AllowedCountries | \PHPUnit_Framework_MockObject_MockObject + */ + private $allowedCountriesMock; + + /** + * @var \Magento\Store\Model\StoreManagerInterface | \PHPUnit_Framework_MockObject_MockObject + */ + private $storeManagerMock; + + /** + * @var CountryWithWebsites + */ + private $countryByWebsite; + + /** + * @var Share | \PHPUnit_Framework_MockObject_MockObject + */ + private $shareConfigMock; + + public function setUp() + { + $this->countriesFactoryMock = + $this->getMockBuilder(\Magento\Directory\Model\ResourceModel\Country\CollectionFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $this->allowedCountriesMock = $this->getMockBuilder(AllowedCountries::class) + ->disableOriginalConstructor() + ->getMock(); + $eavCollectionFactoryMock = + $this->getMockBuilder(\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\CollectionFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $optionsFactoryMock = + $this->getMockBuilder(\Magento\Eav\Model\ResourceModel\Entity\Attribute\OptionFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeManagerMock = $this->getMock(StoreManagerInterface::class); + $this->shareConfigMock = $this->getMockBuilder(Share::class) + ->disableOriginalConstructor() + ->getMock(); + $this->countryByWebsite = new CountryWithWebsites( + $eavCollectionFactoryMock, + $optionsFactoryMock, + $this->countriesFactoryMock, + $this->allowedCountriesMock, + $this->storeManagerMock, + $this->shareConfigMock + ); + } + + public function testGetAllOptions() + { + $website1 = $this->getMock(WebsiteInterface::class); + $website2 = $this->getMock(WebsiteInterface::class); + + $website1->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn(1); + $website2->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn(2); + $this->storeManagerMock->expects($this->once()) + ->method('getWebsites') + ->willReturn([$website1, $website2]); + $collectionMock = $this->getMockBuilder(AbstractDb::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->allowedCountriesMock->expects($this->exactly(2)) + ->method('getAllowedCountries') + ->withConsecutive( + ['website', 1], + ['website', 2] + ) + ->willReturnMap([ + ['website', 1, ['AM' => 'AM']], + ['website', 2, ['AM' => 'AM', 'DZ' => 'DZ']] + ]); + $this->countriesFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($collectionMock); + $collectionMock->expects($this->once()) + ->method('addFieldToFilter') + ->with('country_id', ['in' => ['AM' => 'AM', 'DZ' => 'DZ']]) + ->willReturnSelf(); + $collectionMock->expects($this->once()) + ->method('toOptionArray') + ->willReturn([ + ['value' => 'AM', 'label' => 'UZ'] + ]); + + $this->assertEquals([ + ['value' => 'AM', 'label' => 'UZ', 'website_ids' => [1, 2]] + ], $this->countryByWebsite->getAllOptions()); + } +} diff --git a/app/code/Magento/Customer/etc/di.xml b/app/code/Magento/Customer/etc/di.xml index ebde958f75bc1f678aafd7ccd677c1536f76ef69..b45d00ad0cdddcc1c46645569b92c7848feb8cff 100644 --- a/app/code/Magento/Customer/etc/di.xml +++ b/app/code/Magento/Customer/etc/di.xml @@ -311,6 +311,9 @@ <type name="Magento\Customer\Api\CustomerRepositoryInterface"> <plugin name="transactionWrapper" type="Magento\Customer\Model\Plugin\CustomerRepository\TransactionWrapper" sortOrder="-1"/> </type> + <type name="Magento\Directory\Model\AllowedCountries"> + <plugin name="customerAllowedCountries" type="Magento\Customer\Model\Plugin\AllowedCountries"/> + </type> <type name="Magento\Framework\App\Action\AbstractAction"> <plugin name="customerNotification" type="Magento\Customer\Model\Plugin\CustomerNotification"/> </type> diff --git a/app/code/Magento/Customer/etc/module.xml b/app/code/Magento/Customer/etc/module.xml index 085320e80d2c68e20fd832c21626da841ad264e1..fd8307fc36657d8aacc419b2ad48006969cf68f0 100644 --- a/app/code/Magento/Customer/etc/module.xml +++ b/app/code/Magento/Customer/etc/module.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_Customer" setup_version="2.0.8"> + <module name="Magento_Customer" setup_version="2.0.9"> <sequence> <module name="Magento_Eav"/> <module name="Magento_Directory"/> diff --git a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml index 41ffb74184badc80bfa823e294b2ebde1899952b..61d2900af4bcbc19be1417c99d1d3ecb3f9bdf30 100644 --- a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml +++ b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml @@ -98,7 +98,11 @@ <item name="config" xsi:type="array"> <item name="dataType" xsi:type="string">number</item> <item name="formElement" xsi:type="string">select</item> + <item name="component" xsi:type="string">Magento_Ui/js/form/element/website</item> <item name="source" xsi:type="string">customer</item> + <item name="imports" xsi:type="array"> + <item name="customerId" xsi:type="string">${ $.provider }:data.customer.entity_id</item> + </item> <item name="validation" xsi:type="array"> <item name="required-entry" xsi:type="boolean">true</item> </item> @@ -256,6 +260,10 @@ <item name="label" xsi:type="string" translate="true">Send Welcome Email From</item> <item name="dataType" xsi:type="string">number</item> <item name="formElement" xsi:type="string">select</item> + <item name="source" xsi:type="string">customer</item> + <item name="imports" xsi:type="array"> + <item name="value" xsi:type="string">${ $.provider }:data.customer.store_id</item> + </item> </item> </argument> </field> @@ -369,10 +377,14 @@ <item name="config" xsi:type="array"> <item name="dataType" xsi:type="string">text</item> <item name="formElement" xsi:type="string">select</item> + <item name="component" xsi:type="string">Magento_Ui/js/form/element/country</item> <item name="source" xsi:type="string">address</item> <item name="validation" xsi:type="array"> <item name="required-entry" xsi:type="boolean">true</item> </item> + <item name="imports" xsi:type="array"> + <item name="default" xsi:type="string">${ $.provider }:data.customer.website_id</item> + </item> </item> </argument> </field> 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..57c6a78e6b572026cc451a4cf28bfc6e8c450e2d --- /dev/null +++ b/app/code/Magento/Deploy/Model/DeployManager.php @@ -0,0 +1,208 @@ +<?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; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +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.'); + } else { + $version = (new \DateTime())->getTimestamp(); + $this->versionStorage->save($version); + } + + /** @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(); + if (!$this->idDryRun) { + $this->output->writeln("New version of deployed files: {$version}"); + } + + 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; + } +} 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/Directory/Model/AllowedCountries.php b/app/code/Magento/Directory/Model/AllowedCountries.php new file mode 100644 index 0000000000000000000000000000000000000000..9c8b0c51c2e0ee280527858d8bb9639bf4908385 --- /dev/null +++ b/app/code/Magento/Directory/Model/AllowedCountries.php @@ -0,0 +1,150 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Directory\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Class CountryHandler + */ +class AllowedCountries +{ + const ALLOWED_COUNTRIES_PATH = 'general/country/allow'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @return void + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + StoreManagerInterface $storeManager + ) { + $this->scopeConfig = $scopeConfig; + $this->storeManager = $storeManager; + } + + /** + * Retrieve all allowed countries for scope or scopes + * + * @param string | null $scopeCode + * @param string $scope + * @return array + */ + public function getAllowedCountries( + $scope = ScopeInterface::SCOPE_WEBSITE, + $scopeCode = null + ) { + if (empty($scopeCode)) { + $scopeCode = $this->getDefaultScopeCode($scope); + } + + switch ($scope) { + case ScopeInterface::SCOPE_WEBSITES: + case ScopeInterface::SCOPE_STORES: + $allowedCountries = []; + foreach ($scopeCode as $singleFilter) { + $allowedCountries = array_merge( + $allowedCountries, + $this->getCountriesFromConfig($this->getSingleScope($scope), $singleFilter) + ); + } + break; + default: + $allowedCountries = $this->getCountriesFromConfig($scope, $scopeCode); + } + + return $this->makeCountriesUnique($allowedCountries); + } + + /** + * Resolve scope code by scope + * + * @throws \InvalidArgumentException + * @param string $scope + * @return array|int + */ + private function getDefaultScopeCode($scope) + { + switch ($scope) { + case ScopeInterface::SCOPE_WEBSITE: + return $this->storeManager->getWebsite()->getId(); + case ScopeInterface::SCOPE_STORE: + return $this->storeManager->getStore()->getId(); + case ScopeInterface::SCOPE_GROUP: + return $this->storeManager->getGroup()->getId(); + case ScopeInterface::SCOPE_WEBSITES: + return [$this->storeManager->getWebsite()->getId()]; + case ScopeInterface::SCOPE_STORES: + return [$this->storeManager->getStore()->getId()]; + default: + throw new \InvalidArgumentException("Invalid scope is specified"); + } + } + + /** + * Return Unique Countries by merging them by keys + * + * @param array $allowedCountries + * @return array + */ + public function makeCountriesUnique(array $allowedCountries) + { + return array_combine($allowedCountries, $allowedCountries); + } + + /** + * Takes countries from Countries Config data + * + * @param string $scope + * @param int $scopeCode + * @return array + */ + public function getCountriesFromConfig($scope, $scopeCode) + { + return explode( + ',', + (string) $this->scopeConfig->getValue( + self::ALLOWED_COUNTRIES_PATH, + $scope, + $scopeCode + ) + ); + } + + /** + * Return Single Scope + * + * @param string $scope + * @return string + */ + private function getSingleScope($scope) + { + if ($scope == ScopeInterface::SCOPE_WEBSITES) { + return ScopeInterface::SCOPE_WEBSITE; + } + + if ($scope == ScopeInterface::SCOPE_STORES) { + return ScopeInterface::SCOPE_STORE; + } + + return $scope; + } +} diff --git a/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php b/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php index 70dbfe9b2b9bcb2b3ffa2944a36e5dd93cce2b57..1093e41b6da61037732eb40c29c40dfea78cb671 100644 --- a/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php +++ b/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php @@ -10,6 +10,9 @@ * Directory Country Resource Collection */ namespace Magento\Directory\Model\ResourceModel\Country; +use Magento\Directory\Model\AllowedCountries; +use Magento\Framework\App\ObjectManager; +use Magento\Store\Model\ScopeInterface; /** * Class Collection @@ -53,6 +56,11 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab */ protected $helperData; + /** + * @var AllowedCountries + */ + private $allowedCountriesReader; + /** * @var string[] */ @@ -118,6 +126,21 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab $this->_init(\Magento\Directory\Model\Country::class, \Magento\Directory\Model\ResourceModel\Country::class); } + /** + * Return Allowed Countries reader + * + * @deprecated + * @return \Magento\Directory\Model\AllowedCountries + */ + private function getAllowedCountriesReader() + { + if (!$this->allowedCountriesReader) { + $this->allowedCountriesReader = ObjectManager::getInstance()->get(AllowedCountries::class); + } + + return $this->allowedCountriesReader; + } + /** * Load allowed countries for current store * @@ -126,16 +149,13 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab */ public function loadByStore($store = null) { - $allowCountries = explode(',', - (string)$this->_scopeConfig->getValue( - 'general/country/allow', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $store - ) - ); - if (!empty($allowCountries)) { - $this->addFieldToFilter("country_id", ['in' => $allowCountries]); + $allowedCountries = $this->getAllowedCountriesReader() + ->getAllowedCountries(ScopeInterface::SCOPE_STORE, $store); + + if (!empty($allowedCountries)) { + $this->addFieldToFilter("country_id", ['in' => $allowedCountries]); } + return $this; } diff --git a/app/code/Magento/Directory/Test/Unit/Model/AllowedCountriesTest.php b/app/code/Magento/Directory/Test/Unit/Model/AllowedCountriesTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f5ed44753c43c40305f14895a60b7932818cdea0 --- /dev/null +++ b/app/code/Magento/Directory/Test/Unit/Model/AllowedCountriesTest.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Directory\Test\Unit\Model; + +use Magento\Directory\Model\AllowedCountries; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; + +class AllowedCountriesTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject | ScopeConfigInterface + */ + private $scopeConfigMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject | StoreManagerInterface + */ + private $storeManagerMock; + + /** + * @var AllowedCountries + */ + private $allowedCountriesReader; + + public function setUp() + { + $this->scopeConfigMock = $this->getMock(ScopeConfigInterface::class); + $this->storeManagerMock = $this->getMock(StoreManagerInterface::class); + + $this->allowedCountriesReader = new AllowedCountries( + $this->scopeConfigMock, + $this->storeManagerMock + ); + } + + public function testGetAllowedCountriesWithEmptyFilter() + { + $website1 = $this->getMock(WebsiteInterface::class); + $website1->expects($this->once()) + ->method('getId') + ->willReturn(1); + $this->storeManagerMock->expects($this->once()) + ->method('getWebsite') + ->willReturn($website1); + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->with(AllowedCountries::ALLOWED_COUNTRIES_PATH, 'website', 1) + ->willReturn('AM'); + + $this->assertEquals(['AM' => 'AM'], $this->allowedCountriesReader->getAllowedCountries()); + } + + public function testGetAllowedCountries() + { + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->with(AllowedCountries::ALLOWED_COUNTRIES_PATH, 'website', 1) + ->willReturn('AM'); + + $this->assertEquals( + ['AM' => 'AM'], + $this->allowedCountriesReader->getAllowedCountries(ScopeInterface::SCOPE_WEBSITE, true) + ); + } +} 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/Block/Adminhtml/Order/Create/Form/Address.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php index bcaca17ad3e1f24a71dc6dc54a0e1614a5a2ccb3..c421f511c1671987f49344b5badb7cf5a169d2e1 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php @@ -5,6 +5,8 @@ */ namespace Magento\Sales\Block\Adminhtml\Order\Create\Form; +use Magento\Backend\Model\Session\Quote; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Form\Element\AbstractElement; use Magento\Framework\Pricing\PriceCurrencyInterface; @@ -75,6 +77,16 @@ class Address extends \Magento\Sales\Block\Adminhtml\Order\Create\Form\AbstractF */ protected $addressMapper; + /** + * @var \Magento\Directory\Model\ResourceModel\Country\Collection + */ + private $countriesCollection; + + /** + * @var \Magento\Backend\Model\Session\Quote + */ + private $backendQuoteSession; + /** * Constructor * @@ -263,7 +275,7 @@ class Address extends \Magento\Sales\Block\Adminhtml\Order\Create\Form\AbstractF $this->directoryHelper->getDefaultCountry($this->getStore()) ); } - + $this->processCountryOptions($this->_form->getElement('country_id')); // Set custom renderer for VAT field if needed $vatIdElement = $this->_form->getElement('vat_id'); if ($vatIdElement && $this->getDisplayVatValidationButton() !== false) { @@ -279,6 +291,49 @@ class Address extends \Magento\Sales\Block\Adminhtml\Order\Create\Form\AbstractF return $this; } + /** + * @param \Magento\Framework\Data\Form\Element\AbstractElement $countryElement + * @return void + */ + private function processCountryOptions(\Magento\Framework\Data\Form\Element\AbstractElement $countryElement) + { + $storeId = $this->getBackendQuoteSession()->getStoreId(); + $options = $this->getCountriesCollection() + ->loadByStore($storeId) + ->toOptionArray(); + + $countryElement->setValues($options); + } + + /** + * Retrieve Directiry Countries collection + * @deprecated + * @return \Magento\Directory\Model\ResourceModel\Country\Collection + */ + private function getCountriesCollection() + { + if (!$this->countriesCollection) { + $this->countriesCollection = ObjectManager::getInstance() + ->get(\Magento\Directory\Model\ResourceModel\Country\Collection::class); + } + + return $this->countriesCollection; + } + + /** + * Retrieve Backend Quote Session + * @deprecated + * @return Quote + */ + private function getBackendQuoteSession() + { + if (!$this->backendQuoteSession) { + $this->backendQuoteSession = ObjectManager::getInstance()->get(Quote::class); + } + + return $this->backendQuoteSession; + } + /** * Add additional data to form element * 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/Theme/etc/config.xml b/app/code/Magento/Theme/etc/config.xml index 5fa88578183d453339cf0bd9a529eb82402b3b1f..98df41096daef12e399e2ec67a93bbf94ff5b62c 100644 --- a/app/code/Magento/Theme/etc/config.xml +++ b/app/code/Magento/Theme/etc/config.xml @@ -64,5 +64,10 @@ Disallow: /*SID= </allowed_resources> </media_storage_configuration> </system> + <dev> + <static> + <sign>1</sign> + </static> + </dev> </default> </config> diff --git a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows.js b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows.js index 51f5973372d6910b6fcdd5c36baf68428a389453..449418f07c100c91e139231c704bb28fd8c9ae82 100644 --- a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows.js +++ b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows.js @@ -316,9 +316,11 @@ define([ }, this); } - this.pagesChanged[this.currentPage()] = - !compareArrays(this.defaultPagesState[this.currentPage()], this.arrayFilter(this.getChildItems())); - this.changed(_.some(this.pagesChanged)); + if (this.defaultPagesState[this.currentPage()]) { + this.pagesChanged[this.currentPage()] = + !compareArrays(this.defaultPagesState[this.currentPage()], this.arrayFilter(this.getChildItems())); + this.changed(_.some(this.pagesChanged)); + } }, /** diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/country.js b/app/code/Magento/Ui/view/base/web/js/form/element/country.js new file mode 100644 index 0000000000000000000000000000000000000000..c72d02c595bb8d8d4185f8bc9969cfa3a3bc5940 --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/js/form/element/country.js @@ -0,0 +1,48 @@ +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'underscore', + 'uiRegistry', + './select' +], function (_, registry, Select) { + 'use strict'; + + return Select.extend({ + defaults: { + imports: { + update: '${ $.parentName }.website_id:value' + } + }, + + /** + * Filters 'initialOptions' property by 'field' and 'value' passed, + * calls 'setOptions' passing the result to it + * + * @param {*} value + * @param {String} field + */ + filter: function (value, field) { + var result; + + if (!field) { //validate field, if we are on update + field = this.filterBy.field; + } + + this._super(value, field); + result = _.filter(this.initialOptions, function (item) { + + if (item[field]) { + return ~item[field].indexOf(value); + } + + return false; + }); + + this.setOptions(result); + } + }); +}); + diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/region.js b/app/code/Magento/Ui/view/base/web/js/form/element/region.js index b8cbdcb655a1e7b351adb92d529ff6ae3a3c378b..6414b90ad02eedb2bdf7bfed86ebdab892de0647 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/region.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/region.js @@ -56,16 +56,20 @@ define([ */ filter: function (value, field) { var country = registry.get(this.parentName + '.' + 'country_id'), + option; + + if (country) { option = country.indexedOptions[value]; - this._super(value, field); + this._super(value, field); - if (option && option['is_region_visible'] === false) { - // hide select and corresponding text input field if region must not be shown for selected country - this.setVisible(false); + if (option && option['is_region_visible'] === false) { + // hide select and corresponding text input field if region must not be shown for selected country + this.setVisible(false); - if (this.customEntry) { - this.toggleInput(false); + if (this.customEntry) { + this.toggleInput(false); + } } } } diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/website.js b/app/code/Magento/Ui/view/base/web/js/form/element/website.js new file mode 100644 index 0000000000000000000000000000000000000000..095e2c4740305dbae338e8e3946e82404683b0dc --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/js/form/element/website.js @@ -0,0 +1,33 @@ +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'underscore', + 'uiRegistry', + './select' +], function (_, registry, Select) { + 'use strict'; + + return Select.extend({ + defaults: { + customerId: null, + isGlobalScope: 0 + }, + + /** + * Website component constructor. + * @returns {exports} + */ + initialize: function () { + this._super(); + + if (this.customerId || this.isGlobalScope) { + this.disable(true); + } + + return this; + } + }); +}); 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/app/code/Magento/Wishlist/Helper/Data.php b/app/code/Magento/Wishlist/Helper/Data.php index d64d1a185f0a2e7871eadc5973eb2cfe9e9853f6..736786e6e30b391e5c6b85b05f4b5ded99e7e112 100644 --- a/app/code/Magento/Wishlist/Helper/Data.php +++ b/app/code/Magento/Wishlist/Helper/Data.php @@ -446,7 +446,13 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper */ protected function _getCartUrlParameters($item) { - return ['item' => is_string($item) ? $item : $item->getWishlistItemId()]; + $params = [ + 'item' => is_string($item) ? $item : $item->getWishlistItemId(), + ]; + if ($item instanceof \Magento\Wishlist\Model\Item) { + $params['qty'] = $item->getQty(); + } + return $params; } /** diff --git a/app/code/Magento/Wishlist/Test/Unit/Helper/DataTest.php b/app/code/Magento/Wishlist/Test/Unit/Helper/DataTest.php index 46054445620ac2286f40e700e5eca1a62bd2b9f6..ec6e959a3d4cadeee9ca5136fc9f919628ab4022 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Helper/DataTest.php @@ -124,6 +124,7 @@ class DataTest extends \PHPUnit_Framework_TestCase ->setMethods([ 'getProduct', 'getWishlistItemId', + 'getQty', ]) ->getMock(); @@ -217,6 +218,7 @@ class DataTest extends \PHPUnit_Framework_TestCase $url = 'result url'; $storeId = 1; $wishlistItemId = 1; + $wishlistItemQty = 1; $this->wishlistItem->expects($this->once()) ->method('getProduct') @@ -224,6 +226,9 @@ class DataTest extends \PHPUnit_Framework_TestCase $this->wishlistItem->expects($this->once()) ->method('getWishlistItemId') ->willReturn($wishlistItemId); + $this->wishlistItem->expects($this->once()) + ->method('getQty') + ->willReturn($wishlistItemQty); $this->product->expects($this->once()) ->method('isVisibleInSiteVisibility') @@ -243,9 +248,13 @@ class DataTest extends \PHPUnit_Framework_TestCase ->with('wishlist/index/cart') ->willReturn($url); + $expected = [ + 'item' => $wishlistItemId, + 'qty' => $wishlistItemQty, + ]; $this->postDataHelper->expects($this->once()) ->method('getPostData') - ->with($url, ['item' => $wishlistItemId]) + ->with($url, $expected) ->willReturn($url); $this->assertEquals($url, $this->model->getAddToCartParams($this->wishlistItem)); @@ -256,6 +265,7 @@ class DataTest extends \PHPUnit_Framework_TestCase $url = 'result url'; $storeId = 1; $wishlistItemId = 1; + $wishlistItemQty = 1; $referer = 'referer'; $refererEncoded = 'referer_encoded'; @@ -265,6 +275,9 @@ class DataTest extends \PHPUnit_Framework_TestCase $this->wishlistItem->expects($this->once()) ->method('getWishlistItemId') ->willReturn($wishlistItemId); + $this->wishlistItem->expects($this->once()) + ->method('getQty') + ->willReturn($wishlistItemQty); $this->product->expects($this->once()) ->method('isVisibleInSiteVisibility') @@ -288,9 +301,14 @@ class DataTest extends \PHPUnit_Framework_TestCase ->with('wishlist/index/cart') ->willReturn($url); + $expected = [ + 'item' => $wishlistItemId, + ActionInterface::PARAM_NAME_URL_ENCODED => $refererEncoded, + 'qty' => $wishlistItemQty, + ]; $this->postDataHelper->expects($this->once()) ->method('getPostData') - ->with($url, ['item' => $wishlistItemId, ActionInterface::PARAM_NAME_URL_ENCODED => $refererEncoded]) + ->with($url, $expected) ->willReturn($url); $this->assertEquals($url, $this->model->getAddToCartParams($this->wishlistItem, true)); @@ -363,6 +381,7 @@ class DataTest extends \PHPUnit_Framework_TestCase $url = 'result url'; $storeId = 1; $wishlistItemId = 1; + $wishlistItemQty = 1; $this->wishlistItem->expects($this->once()) ->method('getProduct') @@ -370,6 +389,9 @@ class DataTest extends \PHPUnit_Framework_TestCase $this->wishlistItem->expects($this->once()) ->method('getWishlistItemId') ->willReturn($wishlistItemId); + $this->wishlistItem->expects($this->once()) + ->method('getQty') + ->willReturn($wishlistItemQty); $this->product->expects($this->once()) ->method('isVisibleInSiteVisibility') @@ -383,9 +405,13 @@ class DataTest extends \PHPUnit_Framework_TestCase ->with('wishlist/shared/cart') ->willReturn($url); + $exptected = [ + 'item' => $wishlistItemId, + 'qty' => $wishlistItemQty, + ]; $this->postDataHelper->expects($this->once()) ->method('getPostData') - ->with($url, ['item' => $wishlistItemId]) + ->with($url, $exptected) ->willReturn($url); $this->assertEquals($url, $this->model->getSharedAddToCartUrl($this->wishlistItem)); diff --git a/app/code/Magento/Wishlist/view/frontend/web/wishlist.js b/app/code/Magento/Wishlist/view/frontend/web/wishlist.js index a4fdc178c704f340e77f109110d63a4b0bde5dc3..0d6e510e5f5e955e54933f1a737f35b0da8fc5b6 100644 --- a/app/code/Magento/Wishlist/view/frontend/web/wishlist.js +++ b/app/code/Magento/Wishlist/view/frontend/web/wishlist.js @@ -47,6 +47,7 @@ define([ event.preventDefault(); $.mage.dataPost().postData($(event.currentTarget).data('post-remove')); }, this)) + .on('click', this.options.addToCartSelector, $.proxy(this._beforeAddToCart, this)) .on('click', this.options.addAllToCartSelector, $.proxy(this._addAllWItemsToCart, this)) .on('focusin focusout', this.options.commentInputType, $.proxy(this._focusComment, this)); } @@ -59,6 +60,27 @@ define([ }); }, + /** + * Process data before add to cart + * + * - update item's qty value. + * + * @param {Event} event + * @private + */ + _beforeAddToCart: function(event) { + var elem = $(event.currentTarget), + itemId = elem.data(this.options.dataAttribute), + qtyName = $.validator.format(this.options.nameFormat, itemId), + qtyValue = elem.parents().find('[name="' + qtyName + '"]').val(), + params = elem.data('post'); + + if (params) { + params.data = $.extend({}, params.data, {'qty': qtyValue}); + elem.data('post', params); + } + }, + /** * Add wish list items to cart. * @private 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/bootstrap.php b/dev/tests/functional/bootstrap.php index 72dc6bf208cd0e5dff754f515db9f3a1a5b9a4c7..1ff4fd4e8d22396a71e3413cf9e4247b5225f0b2 100644 --- a/dev/tests/functional/bootstrap.php +++ b/dev/tests/functional/bootstrap.php @@ -11,7 +11,15 @@ defined('MTF_STATES_PATH') || define('MTF_STATES_PATH', MTF_BP . '/lib/Magento/M require_once __DIR__ . '/../../../app/bootstrap.php'; restore_error_handler(); -require_once __DIR__ . '/vendor/autoload.php'; +$vendorAutoload = __DIR__ . '/vendor/autoload.php'; + +if (isset($composerAutoloader)) { + /** var $mtfComposerAutoload \Composer\Autoload\ClassLoader */ + $mtfComposerAutoload = include $vendorAutoload; + $composerAutoloader->addClassMap($mtfComposerAutoload->getClassMap()); +} else { + $composerAutoloader = include $vendorAutoload; +} setCustomErrorHandler(); diff --git a/dev/tests/functional/composer.json b/dev/tests/functional/composer.json index 936c4b968af3b88ed9f05fb2d21dfe3c8184181f..512c5bc8d5df666ced655806af0da64952d0f4ef 100644 --- a/dev/tests/functional/composer.json +++ b/dev/tests/functional/composer.json @@ -1,8 +1,8 @@ { "require": { - "magento/mtf": "1.0.0-rc47", + "magento/mtf": "1.0.0-rc48", "php": "~5.6.0|7.0.2|~7.0.6", - "phpunit/phpunit": "4.1.0", + "phpunit/phpunit": "~4.8.0|~5.5.0", "phpunit/phpunit-selenium": ">=1.2" }, "suggest": { diff --git a/dev/tests/functional/credentials.xml.dist b/dev/tests/functional/credentials.xml.dist index 3c61630fb148df35b67d7430c961bcfc9482cbb1..88794d183e8781d5cea1c9199370c67daae1585c 100644 --- a/dev/tests/functional/credentials.xml.dist +++ b/dev/tests/functional/credentials.xml.dist @@ -27,4 +27,36 @@ <field replace="carriers_dhl_id_eu" value="" /> <field replace="carriers_dhl_password_eu" value="" /> <field replace="carriers_dhl_account_eu" value="" /> + + <field path="payment/authorizenet_directpost/login" value="" /> + <field path="payment/authorizenet_directpost/trans_key" value="" /> + <field path="payment/authorizenet_directpost/trans_md5" value="" /> + + <field path="payment/braintree_section/braintree/braintree_advanced/merchant_account_id" value="" /> + <field path="payment/braintree_section/braintree/braintree_required/merchant_id" value="" /> + <field path="payment/braintree_section/braintree/braintree_required/public_key" value="" /> + <field path="payment/braintree_section/braintree/braintree_required/private_key" value="" /> + + <field path="payment/paypal_group_all_in_one/wpp_usuk/wpp_required_settings/wpp_and_express_checkout/business_account" value="" /> + <field path="payment/paypal_group_all_in_one/wpp_usuk/wpp_required_settings/wpp_and_express_checkout/api_username" value="" /> + <field path="payment/paypal_group_all_in_one/wpp_usuk/wpp_required_settings/wpp_and_express_checkout/api_password" value="" /> + <field path="payment/paypal_group_all_in_one/wpp_usuk/wpp_required_settings/wpp_and_express_checkout/api_signature" value="" /> + <field path="payment/paypal_express/merchant_id" value="" /> + + <field path="payment/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_required/paypal_payflow_api_settings/business_account" value="" /> + <field path="payment/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_required/paypal_payflow_api_settings/partner" value="" /> + <field path="payment/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_required/paypal_payflow_api_settings/user" value="" /> + <field path="payment/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_required/paypal_payflow_api_settings/pwd" value="" /> + <field path="payment/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_required/paypal_payflow_api_settings/vendor" value="" /> + + <field path="payment/paypal_alternative_payment_methods/express_checkout_us/express_checkout_required/express_checkout_required_express_checkout/business_account" value="" /> + <field path="payment/paypal_alternative_payment_methods/express_checkout_us/express_checkout_required/express_checkout_required_express_checkout/api_username" value="" /> + <field path="payment/paypal_alternative_payment_methods/express_checkout_us/express_checkout_required/express_checkout_required_express_checkout/api_password" value="" /> + <field path="payment/paypal_alternative_payment_methods/express_checkout_us/express_checkout_required/express_checkout_required_express_checkout/api_signature" value="" /> + + <field path="payment/paypal_payment_gateways/payflow_link_us/payflow_link_required/payflow_link_payflow_link/business_account" value="" /> + <field path="payment/paypal_payment_gateways/payflow_link_us/payflow_link_required/payflow_link_payflow_link/partner" value="" /> + <field path="payment/paypal_payment_gateways/payflow_link_us/payflow_link_required/payflow_link_payflow_link/user" value="" /> + <field path="payment/paypal_payment_gateways/payflow_link_us/payflow_link_required/payflow_link_payflow_link/pwd" value="" /> + <field path="payment/paypal_payment_gateways/payflow_link_us/payflow_link_required/payflow_link_payflow_link/vendor" value="" /> </replace> diff --git a/dev/tests/functional/lib/Magento/Mtf/Client/Element/RadiobuttonElement.php b/dev/tests/functional/lib/Magento/Mtf/Client/Element/RadiobuttonElement.php new file mode 100644 index 0000000000000000000000000000000000000000..cfb7990196598e317d3ec51b41b1524ac62a7fb5 --- /dev/null +++ b/dev/tests/functional/lib/Magento/Mtf/Client/Element/RadiobuttonElement.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Mtf\Client\Element; + +use Magento\Mtf\Client\Locator; + +/** + * Class provides ability to work with page element radio button. + */ +class RadiobuttonElement extends SimpleElement +{ + /** + * Label for radio button selector. + * + * @var string + */ + protected $labelSelector = './..//label[contains(., "%s")]'; + + /** + * Selector for selected label. + * + * @var string + */ + protected $selectedLabelSelector = 'input[type=radio]:checked + label'; + + /** + * Get value of the required element. + * + * @return string + */ + public function getValue() + { + $this->eventManager->dispatchEvent(['get_value'], [$this->getAbsoluteSelector()]); + + return $this->find($this->selectedLabelSelector)->getText(); + } + + /** + * Select radio button based on label value. + * + * @param string $value + * @return void + */ + public function setValue($value) + { + $this->eventManager->dispatchEvent(['set_value'], [__METHOD__, $this->getAbsoluteSelector()]); + + $radioButtonLabel = $this->find(sprintf($this->labelSelector, $value), Locator::SELECTOR_XPATH); + if (!$this->isSelected()) { + $radioButtonLabel->click(); + } + } +} 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/Bundle/Test/Constraint/AssertBundleProductInCart.php b/dev/tests/functional/tests/app/Magento/Bundle/Test/Constraint/AssertBundleProductInCart.php new file mode 100644 index 0000000000000000000000000000000000000000..831d2e88b37cbeabab87108ffab56f63668c6f63 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/Constraint/AssertBundleProductInCart.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Bundle\Test\Constraint; + +use Magento\Catalog\Test\Constraint\AssertProductInCart; +use Magento\Checkout\Test\Page\CheckoutCart; +use Magento\Mtf\Fixture\FixtureInterface; + +/** + * Assertion that bundle product is correctly displayed in cart. + */ +class AssertBundleProductInCart extends AssertProductInCart +{ + /** + * Count prices. + * + * @param FixtureInterface $product + * @param CheckoutCart $checkoutCart + * @return void + */ + protected function countPrices(FixtureInterface $product, CheckoutCart $checkoutCart) + { + parent::countPrices($product, $checkoutCart); + $this->countSubItemPrice($product); + } + + /** + * Count subItem price. + * + * @param FixtureInterface $product + * @return void + */ + private function countSubItemPrice(FixtureInterface $product) + { + $checkoutData = $product->getCheckoutData(); + if (isset($checkoutData['cartItem']['subItemPrice'])) { + $this->fixtureActualPrice += $checkoutData["cartItem"]["subItemPrice"]; + } + } +} diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/Constraint/AssertBundleProductPage.php b/dev/tests/functional/tests/app/Magento/Bundle/Test/Constraint/AssertBundleProductPage.php index 704404e665ac1c394d9d28621a0dcb9139dd4047..8e113996bda074b089119339a984ea217b6a6e7a 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/Constraint/AssertBundleProductPage.php +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/Constraint/AssertBundleProductPage.php @@ -46,4 +46,39 @@ class AssertBundleProductPage extends AssertProductPage return empty($errors) ? null : implode("\n", $errors); } + + /** + * Verify product special price is displayed on product page(front-end). + * + * @return string|null + */ + protected function verifySpecialPrice() + { + if (!$this->product->hasData('special_price')) { + return null; + } + + $priceBlock = $this->productView->getPriceBlock(); + + if (!$priceBlock->isVisible()) { + return "Price block for '{$this->product->getName()}' product' is not visible."; + } + + if (!$priceBlock->isOldPriceVisible()) { + return 'Bundle special price is not set.'; + } + + $regularPrice = $priceBlock->getOldPrice(); + $priceData = $this->product->getDataFieldConfig('price')['source']->getPriceData(); + + if (!isset($priceData['regular_from'])) { + return 'Regular from price not set.'; + } + + if ($priceData['regular_from'] != $regularPrice) { + return 'Bundle regular price on product view page is not correct.'; + } + + return null; + } } diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct/BundleSelections.xml b/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct/BundleSelections.xml index 0b142044323d197e72a4c1544334d42cd7e16ae5..3e9b055c48ca5a1bfa5df3410ec48ef708a71fa7 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct/BundleSelections.xml +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct/BundleSelections.xml @@ -150,7 +150,7 @@ <field name="products" xsi:type="array"> <item name="0" xsi:type="array"> <item name="0" xsi:type="string">catalogProductSimple::default</item> - <item name="1" xsi:type="string">catalogProductSimple::product_100_dollar</item> + <item name="1" xsi:type="string">catalogProductSimple::product_10_dollar</item> </item> </field> </dataset> @@ -604,5 +604,66 @@ </item> </field> </dataset> + + <dataset name="dynamic_with_two_required_options_assigned_products_with_special_price"> + <field name="bundle_options" xsi:type="array"> + <item name="0" xsi:type="array"> + <item name="title" xsi:type="string">Drop-down Option</item> + <item name="type" xsi:type="string">Drop-down</item> + <item name="required" xsi:type="string">Yes</item> + <item name="assigned_products" xsi:type="array"> + <item name="0" xsi:type="array"> + <item name="search_data" xsi:type="array"> + <item name="name" xsi:type="string">%product_name%</item> + </item> + <item name="data" xsi:type="array"> + <item name="selection_qty" xsi:type="string">1</item> + </item> + </item> + <item name="1" xsi:type="array"> + <item name="search_data" xsi:type="array"> + <item name="name" xsi:type="string">%product_name%</item> + </item> + <item name="data" xsi:type="array"> + <item name="selection_qty" xsi:type="string">1</item> + </item> + </item> + </item> + </item> + <item name="1" xsi:type="array"> + <item name="title" xsi:type="string">Drop-down Option</item> + <item name="type" xsi:type="string">Drop-down</item> + <item name="required" xsi:type="string">Yes</item> + <item name="assigned_products" xsi:type="array"> + <item name="0" xsi:type="array"> + <item name="search_data" xsi:type="array"> + <item name="name" xsi:type="string">%product_name%</item> + </item> + <item name="data" xsi:type="array"> + <item name="selection_qty" xsi:type="string">1</item> + </item> + </item> + <item name="1" xsi:type="array"> + <item name="search_data" xsi:type="array"> + <item name="name" xsi:type="string">%product_name%</item> + </item> + <item name="data" xsi:type="array"> + <item name="selection_qty" xsi:type="string">1</item> + </item> + </item> + </item> + </item> + </field> + <field name="products" xsi:type="array"> + <item name="0" xsi:type="array"> + <item name="0" xsi:type="string">catalogProductSimple::product_10_dollar</item> + <item name="1" xsi:type="string">catalogProductSimple::product_with_special_price</item> + </item> + <item name="1" xsi:type="array"> + <item name="0" xsi:type="string">catalogProductSimple::product_10_dollar</item> + <item name="1" xsi:type="string">catalogProductSimple::product_with_special_price</item> + </item> + </field> + </dataset> </repository> </config> diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct/CheckoutData.xml b/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct/CheckoutData.xml index 3cc61abfd2e89b3bcfdbe61aac6fbc84dbf461de..97c11284d77c88194a5a5271e31ca7b91a4babf3 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct/CheckoutData.xml +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct/CheckoutData.xml @@ -206,6 +206,36 @@ </field> </dataset> + <dataset name="bundle_with_custom_options_3"> + <field name="options" xsi:type="array"> + <item name="bundle_options" xsi:type="array"> + <item name="0" xsi:type="array"> + <item name="title" xsi:type="string">Drop-down Option</item> + <item name="type" xsi:type="string">Drop-down</item> + <item name="value" xsi:type="array"> + <item name="name" xsi:type="string">product_10_dollar</item> + </item> + </item> + </item> + <item name="custom_options" xsi:type="array"> + <item name="0" xsi:type="array"> + <item name="title" xsi:type="string">attribute_key_0</item> + <item name="value" xsi:type="string">option_key_0</item> + </item> + <item name="1" xsi:type="array"> + <item name="title" xsi:type="string">attribute_key_1</item> + <item name="value" xsi:type="string">option_key_0</item> + </item> + </item> + </field> + <field name="cartItem" xsi:type="array"> + <item name="price" xsi:type="string">100</item> + <item name="qty" xsi:type="string">1</item> + <item name="subtotal" xsi:type="string">100</item> + <item name="subItemPrice" xsi:type="string">10</item> + </field> + </dataset> + <dataset name="bundle_all_types_bundle_fixed_and_custom_options"> <field name="options" xsi:type="array"> <item name="bundle_options" xsi:type="array"> diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct/Price.xml b/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct/Price.xml index c6b69dd6937d9bba0628b6cd7fca47d988cf12fb..c4431c5d02072766003b123ce269298f275d5be9 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct/Price.xml +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct/Price.xml @@ -78,6 +78,18 @@ <field name="cart_price" xsi:type="string">756.00</field> </dataset> + <dataset name="fixed-51"> + <field name="price_from" xsi:type="string">51.00</field> + <field name="price_to" xsi:type="string">52.00</field> + <field name="regular_from" xsi:type="string">135.00</field> + </dataset> + + <dataset name="fixed-756-custom-options"> + <field name="price_from" xsi:type="string">785.00</field> + <field name="price_to" xsi:type="string">786.00</field> + <field name="cart_price" xsi:type="string">786.00</field> + </dataset> + <dataset name="bundle_fixed_with_category"> <field name="price_from" xsi:type="string">130.00</field> <field name="price_to" xsi:type="string">144.00</field> @@ -94,6 +106,12 @@ <field name="price_from" xsi:type="string">8.00</field> <field name="price_to" xsi:type="string">20.00</field> <field name="cart_price" xsi:type="string">80.00</field> + <field name="regular_from" xsi:type="string">40.00</field> + </dataset> + + <dataset name="dynamic-18"> + <field name="price_from" xsi:type="string">18.00</field> + <field name="price_to" xsi:type="string">20.00</field> </dataset> <dataset name="dynamic-32"> diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/CreateBundleProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/CreateBundleProductEntityTest.xml index 6fcef33132603bdcc157dcd683993a5893a2dcfb..678e9306cbadb3bc22dafd9bca089a8b7f304ba7 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/CreateBundleProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/CreateBundleProductEntityTest.xml @@ -160,7 +160,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductOutOfStock" /> <constraint name="Magento\Bundle\Test\Constraint\AssertBundlePriceView" /> </variation> - <variation name="CreateBundleProductEntityTestVariation7"> + <variation name="CreateBundleProductEntityTestVariation7" summary="Check bundle dynamic product with tier price"> <data name="product/data/url_key" xsi:type="string">bundle-product-%isolation%</data> <data name="product/data/name" xsi:type="string">BundleProduct %isolation%</data> <data name="product/data/sku_type" xsi:type="string">Yes</data> @@ -333,5 +333,118 @@ <constraint name="Magento\Bundle\Test\Constraint\AssertBundleInCategory" /> <constraint name="Magento\Bundle\Test\Constraint\AssertBundleProductPage" /> </variation> + <variation name="CreateBundleProductEntityTestVariation17" summary="Create fixed bundle product with tier price and custom options with fixed and percent price"> + <data name="product/data/url_key" xsi:type="string">bundle-product-%isolation%</data> + <data name="product/data/name" xsi:type="string">Bundle Fixed %isolation%</data> + <data name="product/data/sku_type" xsi:type="string">No</data> + <data name="product/data/sku" xsi:type="string">sku_bundle_fixed_%isolation%</data> + <data name="product/data/price_type" xsi:type="string">No</data> + <data name="product/data/price/value" xsi:type="string">100</data> + <data name="product/data/price/dataset" xsi:type="string">fixed-100</data> + <data name="product/data/weight_type" xsi:type="string">No</data> + <data name="product/data/weight" xsi:type="string">10</data> + <data name="product/data/tier_price/dataset" xsi:type="string">default</data> + <data name="product/data/price_view" xsi:type="string">As Low as</data> + <data name="product/data/stock_data/use_config_manage_stock" xsi:type="string">No</data> + <data name="product/data/stock_data/manage_stock" xsi:type="string">No</data> + <data name="product/data/shipment_type" xsi:type="string">Together</data> + <data name="product/data/bundle_selections/dataset" xsi:type="string">second</data> + <data name="product/data/custom_options/dataset" xsi:type="string">percent_and_fixed_drop_down_options</data> + <data name="product/data/checkout_data/dataset" xsi:type="string">bundle_with_custom_options_3</data> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> + <constraint name="Magento\Bundle\Test\Constraint\AssertBundleProductForm" /> + <constraint name="Magento\Bundle\Test\Constraint\AssertBundleItemsOnProductPage" /> + <constraint name="Magento\Bundle\Test\Constraint\AssertTierPriceOnBundleProductPage" /> + <constraint name="Magento\Bundle\Test\Constraint\AssertProductCustomOptionsOnBundleProductPage" /> + <constraint name="Magento\Bundle\Test\Constraint\AssertBundleProductInCart" /> + </variation> + <variation name="CreateBundleProductEntityTestVariation18" summary="Create dynamic bundle product with tier price for sub-item"> + <data name="product/data/url_key" xsi:type="string">bundle-product-%isolation%</data> + <data name="product/data/name" xsi:type="string">BundleProduct %isolation%</data> + <data name="product/data/sku_type" xsi:type="string">Yes</data> + <data name="product/data/sku" xsi:type="string">bundle_sku_%isolation%</data> + <data name="product/data/price_type" xsi:type="string">Yes</data> + <data name="product/data/price/dataset" xsi:type="string">dynamic-50</data> + <data name="product/data/weight_type" xsi:type="string">No</data> + <data name="product/data/weight" xsi:type="string">10</data> + <data name="product/data/tier_price/dataset" xsi:type="string">default</data> + <data name="product/data/price_view" xsi:type="string">As Low as</data> + <data name="product/data/stock_data/use_config_manage_stock" xsi:type="string">No</data> + <data name="product/data/stock_data/manage_stock" xsi:type="string">No</data> + <data name="product/data/shipment_type" xsi:type="string">Together</data> + <data name="product/data/bundle_selections/dataset" xsi:type="string">default_dynamic</data> + <data name="product/data/bundle_selections/products" xsi:type="string">catalogProductSimple::simple_with_tier_price,catalogProductVirtual::product_50_dollar</data> + <data name="product/data/checkout_data/dataset" xsi:type="string">bundle_default</data> + <data name="product/data/visibility" xsi:type="string">Search</data> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> + <constraint name="Magento\Bundle\Test\Constraint\AssertBundleProductForm" /> + <constraint name="Magento\Bundle\Test\Constraint\AssertBundleItemsOnProductPage" /> + <constraint name="Magento\Bundle\Test\Constraint\AssertTierPriceOnBundleProductPage" /> + </variation> + <variation name="CreateBundleProductEntityTestVariation19" summary="Create bundle with simple out of stock"> + <data name="product/data/url_key" xsi:type="string">bundle-product-%isolation%</data> + <data name="product/data/name" xsi:type="string">BundleProduct %isolation%</data> + <data name="product/data/sku" xsi:type="string">bundle_sku_%isolation%</data> + <data name="product/data/price_type" xsi:type="string">Yes</data> + <data name="product/data/price/dataset" xsi:type="string">dynamic-50</data> + <data name="product/data/bundle_selections/dataset" xsi:type="string">default_dynamic</data> + <data name="product/data/bundle_selections/products" xsi:type="string">catalogProductSimple::out_of_stock,catalogProductSimple::out_of_stock</data> + <data name="product/data/checkout_data/dataset" xsi:type="string">bundle_default</data> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductOutOfStock" /> + </variation> + <variation name="CreateBundleProductEntityTestVariation21" summary="Create default fixed bundle with custom Website"> + <data name="product/data/url_key" xsi:type="string">bundle-product-%isolation%</data> + <data name="product/data/name" xsi:type="string">Bundle Fixed %isolation%</data> + <data name="product/data/sku_type" xsi:type="string">No</data> + <data name="product/data/sku" xsi:type="string">sku_bundle_fixed_%isolation%</data> + <data name="product/data/price_type" xsi:type="string">No</data> + <data name="product/data/price/value" xsi:type="string">10</data> + <data name="product/data/bundle_selections/dataset" xsi:type="string">second</data> + <data name="product/data/bundle_selections/products" xsi:type="string">catalogProductSimple::product_100_dollar,catalogProductSimple::product_40_dollar</data> + <data name="product/data/checkout_data/dataset" xsi:type="string">bundle_default</data> + <data name="product/data/website_ids/0/dataset" xsi:type="string">custom_store</data> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductOnCustomWebsite" /> + </variation> + <variation name="CreateBundleProductEntityTestVariation22" summary="Create Bundle (fixed) Product with special price and Custom options with fixed price"> + <data name="product/data/url_key" xsi:type="string">bundle-product-%isolation%</data> + <data name="product/data/name" xsi:type="string">Bundle Fixed %isolation%</data> + <data name="product/data/sku_type" xsi:type="string">No</data> + <data name="product/data/sku" xsi:type="string">sku_bundle_fixed_%isolation%</data> + <data name="product/data/price_type" xsi:type="string">No</data> + <data name="product/data/price/value" xsi:type="string">100</data> + <data name="product/data/price/dataset" xsi:type="string">fixed-51</data> + <data name="product/data/price_view" xsi:type="string">Price Range</data> + <data name="product/data/special_price" xsi:type="string">20</data> + <data name="product/data/bundle_selections/dataset" xsi:type="string">second</data> + <data name="product/data/custom_options/dataset" xsi:type="string">drop_down_with_one_option_fixed_price</data> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> + <constraint name="Magento\Bundle\Test\Constraint\AssertBundleProductPage" /> + </variation> + <variation name="CreateBundleProductEntityTestVariation23" summary="Create Bundle (dynamic) Product with special price"> + <data name="product/data/url_key" xsi:type="string">bundle-product-%isolation%</data> + <data name="product/data/name" xsi:type="string">Bundle Dynamic %isolation%</data> + <data name="product/data/sku_type" xsi:type="string">Yes</data> + <data name="product/data/sku" xsi:type="string">sku_bundle_dynamic_%isolation%</data> + <data name="product/data/price_type" xsi:type="string">Yes</data> + <data name="product/data/price/dataset" xsi:type="string">dynamic-8</data> + <data name="product/data/special_price" xsi:type="string">20</data> + <data name="product/data/bundle_selections/dataset" xsi:type="string">default_dynamic</data> + <data name="product/data/bundle_selections/products" xsi:type="string">catalogProductSimple::product_100_dollar,catalogProductSimple::product_40_dollar</data> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> + <constraint name="Magento\Bundle\Test\Constraint\AssertBundleProductPage" /> + </variation> + <variation name="CreateBundleProductEntityTestVariation24" summary="Create Bundle (dynamic) Product with One of the bundle options has special price"> + <data name="product/data/url_key" xsi:type="string">bundle-product-%isolation%</data> + <data name="product/data/name" xsi:type="string">Bundle Dynamic %isolation%</data> + <data name="product/data/sku_type" xsi:type="string">Yes</data> + <data name="product/data/sku" xsi:type="string">sku_bundle_dynamic_%isolation%</data> + <data name="product/data/price_type" xsi:type="string">Yes</data> + <data name="product/data/price/dataset" xsi:type="string">dynamic-18</data> + <data name="product/data/bundle_selections/dataset" xsi:type="string">dynamic_with_two_required_options_assigned_products_with_special_price</data> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> + <constraint name="Magento\Bundle\Test\Constraint\AssertBundleProductPage" /> + </variation> </testCase> </config> diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/UpdateBundleProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/UpdateBundleProductEntityTest.xml index 1f241c1e321be78e51a04d04c3e2ca2ab4b04ca0..e4f567de39481249ca083398152f237c35b3c2cf 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/UpdateBundleProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/UpdateBundleProductEntityTest.xml @@ -73,5 +73,15 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Bundle\Test\Constraint\AssertBundleProductPage" /> </variation> + <variation name="UpdateBundleProductEntityTestVariation5" summary="Update Bundle (fixed) Product with custom option"> + <data name="originalProduct/dataset" xsi:type="string">bundle_fixed_product</data> + <data name="product/data/url_key" xsi:type="string">bundle-product-%isolation%</data> + <data name="product/data/name" xsi:type="string">bundle_fixed_%isolation%</data> + <data name="product/data/sku" xsi:type="string">bundle_sku_%isolation%</data> + <data name="product/data/price/dataset" xsi:type="string">fixed-756-custom-options</data> + <data name="product/data/custom_options/dataset" xsi:type="string">drop_down_with_one_option_fixed_price</data> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> + <constraint name="Magento\Bundle\Test\Constraint\AssertBundleProductPage" /> + </variation> </testCase> </config> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/CategoryForm.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/CategoryForm.xml index 593ab70e100ff473b6a87c1ea75e6f3af348107d..42f490d76191a524b12c7837ab5958907199fece 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/CategoryForm.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/CategoryForm.xml @@ -134,11 +134,11 @@ <strategy>xpath</strategy> <fields> <schedule_update_from> - <input>text</input> + <input>datepicker</input> <selector>input[name='custom_design_from']</selector> </schedule_update_from> <schedule_update_to> - <input>text</input> + <input>datepicker</input> <selector>input[name='custom_design_to']</selector> </schedule_update_to> </fields> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/Section/ProductGrid.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/Section/ProductGrid.php index e05bf4af830b6f03dfa801fbb18eefff5a366132..eb2a9c4a835ceb8e55156f49a7ebb9d04ef1cc86 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/Section/ProductGrid.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/Section/ProductGrid.php @@ -36,5 +36,5 @@ class ProductGrid extends Grid * * @var string */ - protected $selectItem = 'tbody tr .col-in_category'; + protected $selectItem = 'tbody tr .col-in_category input'; } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductInCart.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductInCart.php index 85514109053b438af1425cf266d11bd8ac596cb3..e698691faa64d41d7f540dac04c5d48658363333 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductInCart.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductInCart.php @@ -14,13 +14,33 @@ use Magento\Mtf\Constraint\AbstractConstraint; use Magento\Mtf\Fixture\FixtureInterface; /** - * Class AssertProductInCart - * Assertion that the product is correctly displayed in cart + * Assertion that the product is correctly displayed in cart. */ class AssertProductInCart extends AbstractConstraint { /** - * Assertion that the product is correctly displayed in cart + * Price on form. + * + * @var string + */ + protected $formPrice; + + /** + * Fixture actual price. + * + * @var string + */ + protected $fixtureActualPrice; + + /** + * Fixture price. + * + * @var string + */ + protected $fixturePrice; + + /** + * Assertion that the product is correctly displayed in cart. * * @param CatalogProductView $catalogProductView * @param FixtureInterface $product @@ -41,58 +61,102 @@ class AssertProductInCart extends AbstractConstraint $catalogProductView->getMessagesBlock()->waitSuccessMessage(); // Check price - $this->assertOnShoppingCart($product, $checkoutCart); + $this->countPrices($product, $checkoutCart); + \PHPUnit_Framework_Assert::assertEquals( + $this->fixtureActualPrice, + $this->formPrice, + 'Product price in shopping cart is not correct.' + ); } /** - * Assert prices on the shopping cart + * Count prices. * * @param FixtureInterface $product * @param CheckoutCart $checkoutCart * @return void + */ + protected function countPrices(FixtureInterface $product, CheckoutCart $checkoutCart) + { + /** @var CatalogProductSimple $product */ + $this->fixturePrice = $product->getPrice(); + $this->prepareFormPrice($product, $checkoutCart); + $this->countSpecialPrice($product); + $this->countCheckoutCartItemPrice($product); + $this->countCustomOptionsPrice($product); + } + + /** + * Count count special price. * - * @SuppressWarnings(PHPMD.NPathComplexity) + * @param FixtureInterface $product + * @return void */ - protected function assertOnShoppingCart(FixtureInterface $product, CheckoutCart $checkoutCart) + protected function countSpecialPrice(FixtureInterface $product) { - $checkoutCart->open(); /** @var CatalogProductSimple $product */ - $customOptions = $product->getCustomOptions(); - $checkoutData = $product->getCheckoutData(); - $checkoutCartItem = isset($checkoutData['cartItem']) ? $checkoutData['cartItem'] : []; - $checkoutCustomOptions = isset($checkoutData['options']['custom_options']) - ? $checkoutData['options']['custom_options'] - : []; - $fixturePrice = $product->getPrice(); $specialPrice = $product->getSpecialPrice(); - $cartItem = $checkoutCart->getCartBlock()->getCartItem($product); - $formPrice = $cartItem->getPrice(); - if ($specialPrice) { - $fixturePrice = $specialPrice; + $this->fixturePrice = $product->getSpecialPrice(); } + } + + /** + * Prepare form price. + * + * @param FixtureInterface $product + * @param CheckoutCart $checkoutCart + * @return void + */ + protected function prepareFormPrice(FixtureInterface $product, CheckoutCart $checkoutCart) + { + $checkoutCart->open(); + $cartItem = $checkoutCart->getCartBlock()->getCartItem($product); + $this->formPrice = $cartItem->getPrice(); + } + + /** + * Count cart item price. + * + * @param FixtureInterface $product + * @return void + */ + protected function countCheckoutCartItemPrice(FixtureInterface $product) + { + /** @var CatalogProductSimple $product */ + $checkoutData = $product->getCheckoutData(); + $checkoutCartItem = isset($checkoutData['cartItem']) ? $checkoutData['cartItem'] : []; if (isset($checkoutCartItem['price'])) { - $fixturePrice = $checkoutCartItem['price']; + $this->fixturePrice = $checkoutCartItem['price']; } - $fixtureActualPrice = $fixturePrice; + $this->fixtureActualPrice = $this->fixturePrice; + } + /** + * Count custom options price. + * + * @param FixtureInterface $product + * @return void + */ + protected function countCustomOptionsPrice(FixtureInterface $product) + { + /** @var CatalogProductSimple $product */ + $customOptions = $product->getCustomOptions(); + $checkoutData = $product->getCheckoutData(); + $checkoutCustomOptions = isset($checkoutData['options']['custom_options']) + ? $checkoutData['options']['custom_options'] + : []; foreach ($checkoutCustomOptions as $checkoutOption) { $attributeKey = str_replace('attribute_key_', '', $checkoutOption['title']); $optionKey = str_replace('option_key_', '', $checkoutOption['value']); $option = $customOptions[$attributeKey]['options'][$optionKey]; if ('Fixed' == $option['price_type']) { - $fixtureActualPrice += $option['price']; + $this->fixtureActualPrice += $option['price']; } else { - $fixtureActualPrice += ($fixturePrice / 100) * $option['price']; + $this->fixtureActualPrice += ($this->fixturePrice / 100) * $option['price']; } } - - \PHPUnit_Framework_Assert::assertEquals( - $fixtureActualPrice, - $formPrice, - 'Product price in shopping cart is not correct.' - ); } /** diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php index e660b0ea05fab3e365f899374a9240e14bdd951c..2ccb804b073f90255f378a6e3ef6e8af5d427332 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php @@ -413,12 +413,12 @@ class Curl extends AbstractCurl implements CatalogProductSimpleInterface { if (!empty($this->fields['product']['website_ids'])) { foreach ($this->fixture->getDataFieldConfig('website_ids')['source']->getWebsites() as $key => $website) { - $this->fields['product']['website_ids'][$key] = $website->getWebsiteId(); + $this->fields['product']['extension_attributes']['website_ids'][$key] = $website->getWebsiteId(); } } else { $website = \Magento\Mtf\ObjectManagerFactory::getObjectManager() ->create(\Magento\Store\Test\Fixture\Website::class, ['dataset' => 'default']); - $this->fields['product']['website_ids'][] = $website->getWebsiteId(); + $this->fields['product']['extension_attributes']['website_ids'][] = $website->getWebsiteId(); } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Webapi.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Webapi.php index 4ec372ab7163bb627bdf1353618f81231bed504d..4c3d344832f85f57d065ccccc01e44e1daf873b1 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Webapi.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Webapi.php @@ -64,13 +64,6 @@ class Webapi extends AbstractWebApi implements CatalogProductSimpleInterface 'custom_attributes' ]; - /** - * Website Ids for current Product. - * - * @var array - */ - private $websiteIds = []; - /** * @constructor * @param DataInterface $configuration @@ -102,8 +95,6 @@ class Webapi extends AbstractWebApi implements CatalogProductSimpleInterface $this->prepareData(); $this->convertData(); - //TODO: Change create and assign product to website flow using 1 request after MAGETWO-52812 delivery. - /** @var CatalogProductSimple $fixture */ $url = $_ENV['app_frontend_url'] . 'rest/all/V1/products'; $this->webapiTransport->write($url, $this->fields, CurlInterface::POST); @@ -116,110 +107,9 @@ class Webapi extends AbstractWebApi implements CatalogProductSimpleInterface throw new \Exception("Product creation by webapi handler was not successful! Response: {$encodedResponse}"); } - $this->assignToWebsites($response); - return $this->parseResponse($response); } - /** - * Assign appropriate Websites to Product and unset all other. - * - * @param array $product - * @return void - */ - private function assignToWebsites($product) - { - $this->setWebsites($product); - $this->unsetWebsites($product); - } - - /** - * Get all Websites. - * - * @return array - * @throws \Exception - */ - private function getAllWebsites() - { - $url = $_ENV['app_frontend_url'] . 'rest/V1/store/websites'; - $this->webapiTransport->write($url, [], CurlInterface::GET); - $encodedResponse = $this->webapiTransport->read(); - $response = json_decode($encodedResponse, true); - $this->webapiTransport->close(); - - if (!isset($response[0]['id'])) { - $this->eventManager->dispatchEvent(['webapi_failed'], [$response]); - throw new \Exception( - "Attempt to get all Websites by webapi handler was not successful! Response: {$encodedResponse}" - ); - } - - return $response; - } - - /** - * Set appropriate Websites to Product. - * - * @param array $product - * @return void - * @throws \Exception - */ - private function setWebsites($product) - { - foreach ($this->websiteIds as $id) { - $url = $_ENV['app_frontend_url'] . 'rest/V1/products/' . $product['sku'] . '/websites'; - $productWebsiteLink = ['productWebsiteLink' => ['website_id' => $id, 'sku' => $product['sku']]]; - $this->webapiTransport->write($url, $productWebsiteLink, CurlInterface::POST); - $encodedResponse = $this->webapiTransport->read(); - $response = json_decode($encodedResponse, true); - $this->webapiTransport->close(); - - if ($response !== true) { - $this->eventManager->dispatchEvent(['webapi_failed'], [$response]); - throw new \Exception( - "Product addition to Website by webapi handler was not successful! Response: {$encodedResponse}" - ); - } - } - } - - /** - * Unset all Websites from Product except appropriate. - * - * @param array $product - * @return void - * @throws \Exception - */ - private function unsetWebsites($product) - { - $allWebsites = $this->getAllWebsites(); - $websiteIds = []; - - foreach ($allWebsites as $website) { - if ($website['code'] == 'admin') { - continue; - } - $websiteIds[] = $website['id']; - } - - $websiteIds = array_diff($websiteIds, $this->websiteIds); - - foreach ($websiteIds as $id) { - $url = $_ENV['app_frontend_url'] . 'rest/V1/products/' . $product['sku'] . '/websites/' . $id; - $this->webapiTransport->write($url, [], CurlInterface::DELETE); - $encodedResponse = $this->webapiTransport->read(); - $response = json_decode($encodedResponse, true); - $this->webapiTransport->close(); - - if ($response !== true) { - $this->eventManager->dispatchEvent(['webapi_failed'], [$response]); - throw new \Exception( - "Product deduction from Website by webapi handler was not successful! Response: {$encodedResponse}" - ); - } - } - } - /** * Prepare data for creating product request. * @@ -244,7 +134,6 @@ class Webapi extends AbstractWebApi implements CatalogProductSimpleInterface protected function convertData() { $fields = []; - $this->websiteIds = $this->fields['product']['website_ids']; unset($this->fields['product']['website_ids']); unset($this->fields['product']['checkout_data']); diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml index 23c13fb1206df158e3f9824651cba9cb6a1ff30e..ecd68593b08cc252e67a144ccd2a9610ac88f22d 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml @@ -1215,7 +1215,9 @@ <item name="dataset" xsi:type="string">taxable_goods</item> </field> <field name="website_ids" xsi:type="array"> - <item name="0" xsi:type="string">Main Website</item> + <item name="0" xsi:type="array"> + <item name="dataset" xsi:type="string">default</item> + </item> </field> <field name="visibility" xsi:type="string">Catalog, Search</field> <field name="url_key" xsi:type="string">simple-product-%isolation%</field> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/Product/CustomOptions.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/Product/CustomOptions.xml index 2b826e84b8e134197fac2a1279ba9bed7363c214..3252f0178e24195e724b45a17021d0528990070a 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/Product/CustomOptions.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/Product/CustomOptions.xml @@ -75,6 +75,37 @@ </field> </dataset> + <dataset name="percent_and_fixed_drop_down_options"> + <field name="0" xsi:type="array"> + <item name="title" xsi:type="string">custom option drop down %isolation%</item> + <item name="is_require" xsi:type="string">Yes</item> + <item name="type" xsi:type="string">Select/Drop-down</item> + <item name="options" xsi:type="array"> + <item name="0" xsi:type="array"> + <item name="title" xsi:type="string">30 bucks</item> + <item name="price" xsi:type="string">30</item> + <item name="price_type" xsi:type="string">Fixed</item> + <item name="sku" xsi:type="string">sku_drop_down_row_1</item> + <item name="sort_order" xsi:type="string">0</item> + </item> + </item> + </field> + <field name="1" xsi:type="array"> + <item name="title" xsi:type="string">custom option drop down 2 %isolation%</item> + <item name="is_require" xsi:type="string">Yes</item> + <item name="type" xsi:type="string">Select/Drop-down</item> + <item name="options" xsi:type="array"> + <item name="0" xsi:type="array"> + <item name="title" xsi:type="string">40 Percent</item> + <item name="price" xsi:type="string">40</item> + <item name="price_type" xsi:type="string">Percent</item> + <item name="sku" xsi:type="string">sku_drop_down_row_1</item> + <item name="sort_order" xsi:type="string">0</item> + </item> + </item> + </field> + </dataset> + <dataset name="drop_down_with_one_option_fixed_price"> <field name="0" xsi:type="array"> <item name="title" xsi:type="string">custom option drop down %isolation%</item> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutTest.xml index 63e232f3dac7215406275df395f8cfd0c5adb667..6a1f225bc9429e47b68ffb85bd06fc0443ebdbf9 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutTest.xml @@ -47,7 +47,7 @@ <constraint name="Magento\Sales\Test\Constraint\AssertOrderButtonsAvailable" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal" /> </variation> - <variation name="OnePageCheckoutTestVariation3" summary="Checkout as UK guest with simple product"> + <variation name="OnePageCheckoutTestVariation3" summary="Checkout as UK guest with simple product" ticketId="MAGETWO-42603"> <data name="products/0" xsi:type="string">catalogProductSimple::default</data> <data name="customer/dataset" xsi:type="string">default</data> <data name="checkoutMethod" xsi:type="string">guest</data> diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Handler/ConfigurableProduct/Curl.php b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Handler/ConfigurableProduct/Curl.php index 402aa6ba0d3184731d43ea3a858d97b42f127816..bf5d5944aa3da0a7093d5bbe33ea00897f3b5397 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Handler/ConfigurableProduct/Curl.php +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Handler/ConfigurableProduct/Curl.php @@ -61,6 +61,24 @@ class Curl extends ProductCurl implements ConfigurableProductInterface return $data; } + /** + * Preparation of websites data. + * + * @return void + */ + protected function prepareWebsites() + { + if (!empty($this->fields['product']['website_ids'])) { + foreach ($this->fixture->getDataFieldConfig('website_ids')['source']->getWebsites() as $key => $website) { + $this->fields['product']['website_ids'][$key] = $website->getWebsiteId(); + } + } else { + $website = \Magento\Mtf\ObjectManagerFactory::getObjectManager() + ->create(\Magento\Store\Test\Fixture\Website::class, ['dataset' => 'default']); + $this->fields['product']['website_ids'][] = $website->getWebsiteId(); + } + } + /** * Preparation of attribute set data. * diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/CustomerForm.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/CustomerForm.xml index 664f96cd4b42f9de8d7ffa4da458ba2a5ada0fc4..bb56a9edaf9a13d7b7fad3c2253e6cebdf5af59e 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/CustomerForm.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/CustomerForm.xml @@ -24,6 +24,9 @@ <gender> <input>select</input> </gender> + <dob> + <input>datepicker</input> + </dob> </fields> </account_information> <addresses> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses.php b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses.php index 947aef9e82cdc3e113a8cb0d13fa01060c529a5b..2a8ecbf80c4165af5a4cfdb385d02acec65d030d 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses.php +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses.php @@ -20,6 +20,7 @@ use Magento\Customer\Test\Fixture\Address; /** * Customer addresses edit block. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Addresses extends Tab { @@ -37,6 +38,8 @@ class Addresses extends Tab */ protected $addressSelector = "//li[address[contains(.,'%s')]]"; + protected $countriesSelector = "//*/select[@name='address[new_%d][country_id]']/option"; + /** * Delete Address button. * @@ -240,6 +243,34 @@ class Addresses extends Tab return $addressTab->isVisible(); } + /** + * Retrieve list of all countries + * @param int $addressNumber + * @return array + */ + public function getCountriesList($addressNumber) + { + $this->openCustomerAddress($addressNumber); + /** @var SimpleElement $element */ + $options = $this->_rootElement->getElements( + sprintf($this->countriesSelector, $addressNumber - 1), + Locator::SELECTOR_XPATH + ); + $data = []; + /** @var SimpleElement $option */ + foreach ($options as $option) { + if ($option->isVisible()) { + $value = $option->getValue(); + + if ($value != "") { + $data[] = $value; + } + } + } + + return $data; + } + /** * Click delete customer address button. * diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Constraint/AssertChangingWebsiteChangeCountries.php b/dev/tests/functional/tests/app/Magento/Customer/Test/Constraint/AssertChangingWebsiteChangeCountries.php new file mode 100644 index 0000000000000000000000000000000000000000..395dac1326dc39536a6b6a69214d74b04e7d2c33 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/Constraint/AssertChangingWebsiteChangeCountries.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Customer\Test\Constraint; + +use Magento\Customer\Test\Fixture\Customer; +use Magento\Customer\Test\Page\Adminhtml\CustomerIndexNew; +use Magento\Mtf\Constraint\AbstractConstraint; + +/** + * Assert required fields on customer form. + */ +class AssertChangingWebsiteChangeCountries extends AbstractConstraint +{ + /** + * Assert required fields on customer form. + * + * @param CustomerIndexNew $customerNewPage + * @param array $expectedRequiredFields + * @return void + */ + public function processAssert( + CustomerIndexNew $customerIndexNew, + Customer $customer, + $expectedList + ) { + $customerIndexNew->getCustomerForm() + ->openTab('account_information'); + $customerIndexNew->getCustomerForm()->fillCustomer($customer); + $customerIndexNew->getCustomerForm() + ->openTab('addresses'); + $tab = $customerIndexNew->getCustomerForm() + ->getTab('addresses'); + $countriesList = $tab->getCountriesList(1); + sort($countriesList); + sort($expectedList); + \PHPUnit_Framework_Assert::assertEquals( + $countriesList, + $expectedList, + 'Wrong country list is displayed.' + ); + } + + /** + * Return string representation of object. + * + * @return string + */ + public function toString() + { + return 'All required fields on customer form are highlighted.'; + } +} diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/CreateCustomerBackendEntityTest.php b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/CreateCustomerBackendEntityTest.php index bf3ae060a6d1b008aab6a94181008aa24149a08c..9d04e9afc68f44ecf450cbf6f97fc3909b76fc15 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/CreateCustomerBackendEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/CreateCustomerBackendEntityTest.php @@ -6,6 +6,11 @@ namespace Magento\Customer\Test\TestCase; +use Magento\Config\Test\Fixture\ConfigData; +use Magento\Customer\Test\Constraint\AssertChangingWebsiteChangeCountries; +use Magento\Framework\App\ObjectManager; +use Magento\Mtf\Fixture\FixtureFactory; +use Magento\Mtf\Fixture\FixtureInterface; use Magento\Mtf\TestCase\Injectable; use Magento\Customer\Test\Fixture\Address; use Magento\Customer\Test\Fixture\Customer; @@ -37,6 +42,14 @@ class CreateCustomerBackendEntityTest extends Injectable */ protected $customer; + /** + * @var Address + */ + private $address; + + /** @var array */ + private $allowedCountriesData = []; + /** * Customer index page. * @@ -51,6 +64,9 @@ class CreateCustomerBackendEntityTest extends Injectable */ protected $pageCustomerIndexNew; + /** @var FixtureFactory */ + private $fixtureFactory; + /** * Inject customer pages. * @@ -60,10 +76,12 @@ class CreateCustomerBackendEntityTest extends Injectable */ public function __inject( CustomerIndex $pageCustomerIndex, - CustomerIndexNew $pageCustomerIndexNew + CustomerIndexNew $pageCustomerIndexNew, + \Magento\Mtf\Fixture\FixtureFactory $fixtureFactory ) { $this->pageCustomerIndex = $pageCustomerIndex; $this->pageCustomerIndexNew = $pageCustomerIndexNew; + $this->fixtureFactory = $fixtureFactory; } /** @@ -74,12 +92,141 @@ class CreateCustomerBackendEntityTest extends Injectable * @param Address $address * @return void */ - public function test(Customer $customer, $customerAction, Address $address = null) - { - // Steps + public function test( + Customer $customer, + $customerAction, + Address $address = null, + array $steps = [], + array $beforeActionCallback = [] + ) { + ///Process initialize steps + foreach ($steps as $methodName => $stepData) { + if (method_exists($this, $methodName)) { + call_user_func_array([$this, $methodName], $stepData); + } + } + $this->pageCustomerIndex->open(); $this->pageCustomerIndex->getPageActionsBlock()->addNew(); $this->pageCustomerIndexNew->getCustomerForm()->fillCustomer($customer, $address); + $this->address = $address; + $this->customer = $customer; + + foreach ($beforeActionCallback as $methodName) { + if (method_exists($this, $methodName)) { + call_user_func([$this, $methodName]); + } + } + $this->pageCustomerIndexNew->getPageActionsBlock()->$customerAction(); } + + /** + * Assert that allowed countries renders in correct way. + * @return void + */ + protected function assertAllowedCountries() + { + /** @var \Magento\Customer\Test\Constraint\AssertChangingWebsiteChangeCountries $assert */ + $assert = $this->objectManager->get( + \Magento\Customer\Test\Constraint\AssertChangingWebsiteChangeCountries::class + ); + + foreach ($this->allowedCountriesData as $dataPerWebsite) { + $customerWithWebsite = $this->fixtureFactory->createByCode( + 'customer', + [ + 'data' => [ + 'website_id' => $dataPerWebsite['website']->getName() + ] + ] + ); + $assert->processAssert( + $this->pageCustomerIndexNew, + $customerWithWebsite, + $dataPerWebsite['countries'] + ); + } + + $this->pageCustomerIndexNew->getCustomerForm()->openTab('account_information'); + $this->pageCustomerIndexNew->getCustomerForm()->fillCustomer($this->customer); + $this->pageCustomerIndexNew->getCustomerForm()->openTab('addresses'); + $this->pageCustomerIndexNew->getCustomerForm()->getTab('addresses')->updateAddresses($this->address); + } + + /** + * @return \Magento\Store\Test\Fixture\Website + */ + private function createWebsiteFixture() + { + /** @var \Magento\Store\Test\Fixture\Website $websiteFixture */ + $websiteFixture = $this->fixtureFactory->createByCode('website', ['dataset' => 'custom_website']); + $websiteFixture->persist(); + $storeGroupFixture = $this->fixtureFactory->createByCode( + 'storeGroup', + [ + 'data' => [ + 'website_id' => [ + 'fixture' => $websiteFixture + ], + 'root_category_id' => [ + 'dataset' => 'default_category' + ], + 'name' => 'Store_Group_%isolation%', + ] + ] + ); + $storeGroupFixture->persist(); + /** @var \Magento\Store\Test\Fixture\Store $storeFixture */ + $storeFixture = $this->fixtureFactory->createByCode( + 'store', + [ + 'data' => [ + 'website_id' => $websiteFixture->getWebsiteId(), + 'group_id' => [ + 'fixture' => $storeGroupFixture + ], + 'is_active' => true, + 'name' => 'Store_%isolation%', + 'code' => 'store_%isolation%' + ] + ] + ); + $storeFixture->persist(); + + return $websiteFixture; + } + + /** + * @param array $countryList + */ + protected function configureAllowedCountries(array $countryList = []) + { + foreach ($countryList as $countries) { + $websiteFixture = $this->createWebsiteFixture(); + /** @var FixtureInterface $configFixture */ + $configFixture = $this->fixtureFactory->createByCode( + 'configData', + [ + 'data' => [ + 'general/country/allow' => [ + 'value' => $countries + ], + 'scope' => [ + 'fixture' => $websiteFixture, + 'scope_type' => 'website', + 'website_id' => $websiteFixture->getWebsiteId(), + 'set_level' => 'website', + ] + ] + ] + ); + + $configFixture->persist(); + $this->allowedCountriesData[] = [ + 'website' => $websiteFixture, + 'countries' => explode(",", $countries) + ]; + } + } } diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/CreateCustomerBackendEntityTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/CreateCustomerBackendEntityTest.xml index 6997cca28fac0a1c25d3c75fecb75bd1e1182ed7..14e3a9642d1a5d649536d0b84070c8ae3e9210a6 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/CreateCustomerBackendEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/CreateCustomerBackendEntityTest.xml @@ -27,7 +27,7 @@ <data name="customer/data/lastname" xsi:type="string">Doe%isolation%</data> <data name="customer/data/suffix" xsi:type="string">S</data> <data name="customer/data/email" xsi:type="string">JohnDoe%isolation%@example.com</data> - <data name="customer/data/dob" xsi:type="string">Mar 16, 2004</data> + <data name="customer/data/dob" xsi:type="string">03/16/2004</data> <data name="customer/data/gender" xsi:type="string">Male</data> <constraint name="Magento\Customer\Test\Constraint\AssertCustomerSuccessSaveMessage" /> <constraint name="Magento\Customer\Test\Constraint\AssertCustomerInGrid" /> @@ -130,5 +130,31 @@ </data> <constraint name="Magento\Customer\Test\Constraint\AssertCustomerBackendRequiredFields" /> </variation> + <variation name="CreateCustomerBackendEntityTestVariation10" summary="Create customer with 2 websites and with different allowed countries."> + <data name="customerAction" xsi:type="string">save</data> + <data name="customer/data/website_id" xsi:type="string">Main Website</data> + <data name="customer/data/group_id/dataset" xsi:type="string">General</data> + <data name="customer/data/firstname" xsi:type="string">John%isolation%</data> + <data name="customer/data/lastname" xsi:type="string">Doe%isolation%</data> + <data name="customer/data/email" xsi:type="string">JohnDoe%isolation%@example.com</data> + <data name="address/data/company" xsi:type="string">Magento</data> + <data name="address/data/country_id" xsi:type="string">Bangladesh</data> + <data name="address/data/street" xsi:type="string">Chmielna 113</data> + <data name="address/data/city" xsi:type="string">Bielsko-Biala</data> + <data name="address/data/postcode" xsi:type="string">43-310</data> + <data name="address/data/telephone" xsi:type="string">799885616</data> + <data name="steps" xsi:type="array"> + <item name="configureAllowedCountries" xsi:type="array"> + <item name="countries" xsi:type="array"> + <item name="0" xsi:type="string">AS,BM</item> + <item name="1" xsi:type="string">BD,BB,AF</item> + </item> + </item> + </data> + <data name="beforeActionCallback" xsi:type="array"> + <item name="assertAllowedCountries" xsi:type="string">assertAllowedCountries</item> + </data> + <constraint name="Magento\Customer\Test\Constraint\AssertCustomerSuccessSaveMessage" /> + </variation> </testCase> </config> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/UpdateCustomerBackendEntityTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/UpdateCustomerBackendEntityTest.xml index ac63dcaf2805eac03fce22b9113e325c890a65b2..c2132672d900a65fe81b3f011266d036cd390e91 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/UpdateCustomerBackendEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/UpdateCustomerBackendEntityTest.xml @@ -16,7 +16,7 @@ <data name="customer/data/lastname" xsi:type="string">Doe%isolation%</data> <data name="customer/data/suffix" xsi:type="string">_Suffix%isolation%</data> <data name="customer/data/email" xsi:type="string">JohnDoe%isolation%@example.com</data> - <data name="customer/data/dob" xsi:type="string">Aug 1, 1986</data> + <data name="customer/data/dob" xsi:type="string">08/01/1986</data> <data name="customer/data/taxvat" xsi:type="string">123456789001</data> <data name="customer/data/gender" xsi:type="string">Male</data> <constraint name="Magento\Customer\Test\Constraint\AssertCustomerSuccessSaveMessage" /> @@ -52,7 +52,7 @@ <data name="customer/data/lastname" xsi:type="string">Doe%isolation%</data> <data name="customer/data/suffix" xsi:type="string">_JaneSuffix%isolation%</data> <data name="customer/data/email" xsi:type="string">Jane%isolation%@example.com</data> - <data name="customer/data/dob" xsi:type="string">Dec 1, 2000</data> + <data name="customer/data/dob" xsi:type="string">01/12/2000</data> <data name="customer/data/taxvat" xsi:type="string">987654321</data> <data name="customer/data/gender" xsi:type="string">Female</data> <data name="address/data/prefix" xsi:type="string">Prefix%isolation%_</data> diff --git a/dev/tests/functional/tests/app/Magento/Install/Test/Block/WebConfiguration.xml b/dev/tests/functional/tests/app/Magento/Install/Test/Block/WebConfiguration.xml index 55c9bd16509c4382b1d4496d361e7d849c5b68e0..2589b105fa8c1ee9cbe6b548b102545bef5b0c7b 100644 --- a/dev/tests/functional/tests/app/Magento/Install/Test/Block/WebConfiguration.xml +++ b/dev/tests/functional/tests/app/Magento/Install/Test/Block/WebConfiguration.xml @@ -13,7 +13,7 @@ <admin /> <keyOwn> <selector>[value="user"]</selector> - <input>checkbox</input> + <input>radiobutton</input> </keyOwn> <keyValue> <selector>[name="key"]</selector> @@ -24,12 +24,12 @@ </apacheRewrites> <httpsFront> <selector>[ng-model*="front"]</selector> - <input>checkbox</input> + <input>radiobutton</input> </httpsFront> <https /> <httpsAdmin> <selector>[type="checkbox"][ng-model*="admin"]</selector> - <input>checkbox</input> + <input>radiobutton</input> </httpsAdmin> </fields> </mapping> diff --git a/dev/tests/functional/tests/app/Magento/Install/Test/TestCase/InstallTest.xml b/dev/tests/functional/tests/app/Magento/Install/Test/TestCase/InstallTest.xml index b5db164511847745de9cc5aaac96532434a4bbd1..8cdf27b35a472cb8f5cff195c1a43fe6fae2673e 100644 --- a/dev/tests/functional/tests/app/Magento/Install/Test/TestCase/InstallTest.xml +++ b/dev/tests/functional/tests/app/Magento/Install/Test/TestCase/InstallTest.xml @@ -7,17 +7,15 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Install\Test\TestCase\InstallTest" summary="[Web Setup][Auto] Install CE Magento via Web Interface" ticketId="MAGETWO-31431"> - <variation name="InstallTestVariation1"> - <data name="description" xsi:type="string">Install with custom admin path.</data> + <variation name="InstallTestVariation1" summary="Install with custom admin path"> <data name="user/dataset" xsi:type="string">default</data> <data name="install/admin" xsi:type="string">custom</data> <constraint name="Magento\Install\Test\Constraint\AssertSuccessInstall" /> <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLogin" /> </variation> - <variation name="InstallTestVariation2"> - <data name="description" xsi:type="string">Install with custom encryption key and changed currency and locale.</data> + <variation name="InstallTestVariation2" summary="Install with custom encryption key and changed currency and locale"> <data name="user/dataset" xsi:type="string">default</data> - <data name="install/keyOwn" xsi:type="string">Yes</data> + <data name="install/keyOwn" xsi:type="string">I want to use my own encryption key</data> <data name="install/keyValue" xsi:type="string">123123qa</data> <data name="install/storeLanguage" xsi:type="string">German (Germany)</data> <data name="install/storeCurrency" xsi:type="string">Euro (EUR)</data> @@ -28,33 +26,29 @@ <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLogin" /> <constraint name="Magento\Install\Test\Constraint\AssertCurrencySelected" /> </variation> - <variation name="InstallTestVariation3"> - <data name="description" xsi:type="string">Install with table prefix.</data> + <variation name="InstallTestVariation3" summary="Install with table prefix"> <data name="user/dataset" xsi:type="string">default</data> <data name="install/dbTablePrefix" xsi:type="string">pref_</data> <data name="install/storeLanguage" xsi:type="string">Chinese (China)</data> <constraint name="Magento\Install\Test\Constraint\AssertSuccessInstall" /> <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLogin" /> </variation> - <variation name="InstallTestVariation4"> - <data name="description" xsi:type="string">Install with enabled url rewrites.</data> + <variation name="InstallTestVariation4" summary="Install with enabled url rewrites"> <data name="user/dataset" xsi:type="string">default</data> <data name="install/apacheRewrites" xsi:type="string">Yes</data> <constraint name="Magento\Install\Test\Constraint\AssertSuccessInstall" /> <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLogin" /> <constraint name="Magento\Install\Test\Constraint\AssertRewritesEnabled" /> </variation> - <variation name="InstallTestVariation5"> - <data name="description" xsi:type="string">Install with enabled secure urls.</data> + <variation name="InstallTestVariation5" summary="Install with enabled secure urls"> <data name="user/dataset" xsi:type="string">default</data> - <data name="install/httpsFront" xsi:type="string">Yes</data> - <data name="install/httpsAdmin" xsi:type="string">Yes</data> + <data name="install/httpsFront" xsi:type="string">Use HTTPS for Magento Storefront</data> + <data name="install/httpsAdmin" xsi:type="string">Use HTTPS for Magento Admin</data> <constraint name="Magento\Install\Test\Constraint\AssertSuccessInstall" /> <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLogin" /> <constraint name="Magento\Install\Test\Constraint\AssertSecureUrlEnabled" /> </variation> - <variation name="InstallTestVariation6"> - <data name="description" xsi:type="string">Install with default values.</data> + <variation name="InstallTestVariation6" summary="Install with default values"> <data name="user/dataset" xsi:type="string">default</data> <constraint name="Magento\Install\Test\Constraint\AssertSuccessInstall" /> <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLogin" /> 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/Review/Test/Block/Adminhtml/Edit/Product/Grid.php b/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Edit/Product/Grid.php new file mode 100644 index 0000000000000000000000000000000000000000..9f7cc54fe88cab5dca510a0682c1c8439040ddaa --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Edit/Product/Grid.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Review\Test\Block\Adminhtml\Edit\Product; + +use Magento\Ui\Test\Block\Adminhtml\DataGrid; + +/** + * Review catalog product grid. + */ +class Grid extends DataGrid +{ + /** + * Grid filter selectors + * + * @var array + */ + protected $filters = [ + 'title' => [ + 'selector' => 'input[name="title"]', + ], + 'sku' => [ + 'selector' => 'input[name="sku"]', + ], + ]; +} diff --git a/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Edit/RatingElement.php b/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Edit/RatingElement.php index 3ba4369fb0fcc9f8d7d63b846577817579dd11ab..afadc1b0858ae072227cebbc0cd721b501182b22 100644 --- a/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Edit/RatingElement.php +++ b/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Edit/RatingElement.php @@ -34,7 +34,7 @@ class RatingElement extends SimpleElement * * @var string */ - protected $ratingByNumber = './/*[@id="rating_detail"]//*[contains(@class,"field-rating")][%d]'; + protected $ratingByNumber = './/*[contains(@class,"field-rating")][%d]'; /** * Set rating value diff --git a/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Product/Edit/Section/Reviews.php b/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Product/Edit/Section/Reviews.php index 37ea3df90a89ffef811be2e6f885ca15b3e80976..5b45f54600ceb2ec0fec9847127146b3952317fa 100644 --- a/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Product/Edit/Section/Reviews.php +++ b/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Product/Edit/Section/Reviews.php @@ -23,12 +23,12 @@ class Reviews extends Section /** * Returns product reviews grid. * - * @return \Magento\Review\Test\Block\Adminhtml\Product\Grid + * @return \Magento\Review\Test\Block\Adminhtml\Edit\Product\Grid */ public function getReviewsGrid() { return $this->blockFactory->create( - \Magento\Review\Test\Block\Adminhtml\Product\Grid::class, + \Magento\Review\Test\Block\Adminhtml\Edit\Product\Grid::class, ['element' => $this->_rootElement->find($this->reviews)] ); } diff --git a/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Product/Grid.php b/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Product/Grid.php index 55060e7a85db92baba5c832d5c6805c687641907..e1aa3b9d5a9a57c46c05c58fa51c622f263ca899 100644 --- a/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Product/Grid.php +++ b/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Product/Grid.php @@ -6,13 +6,20 @@ namespace Magento\Review\Test\Block\Adminhtml\Product; -use Magento\Ui\Test\Block\Adminhtml\DataGrid; +use Magento\Backend\Test\Block\Widget\Grid as AbstractGrid; /** * Review catalog product grid. */ -class Grid extends DataGrid +class Grid extends AbstractGrid { + /** + * First row selector + * + * @var string + */ + protected $firstRowSelector = './/tbody/tr[1]'; + /** * Grid filter selectors * diff --git a/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/ReviewForm.xml b/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/ReviewForm.xml index 1832618a7da64a6122744586e63554805d2b394d..348cd3429149a73e490cff6c6d3a361d52c2a30d 100644 --- a/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/ReviewForm.xml +++ b/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/ReviewForm.xml @@ -20,7 +20,7 @@ <input>textarea</input> </detail> <ratings> - <selector>#detailed_rating</selector> + <selector>#rating_detail</selector> <class>Magento\Review\Test\Block\Adminhtml\Edit\RatingElement</class> </ratings> </fields> diff --git a/dev/tests/functional/tests/app/Magento/Review/Test/Block/ReviewForm.php b/dev/tests/functional/tests/app/Magento/Review/Test/Block/ReviewForm.php index 6bb20785adf91701ee63183764bcefeabef9415b..91ab33e5ca32102ab427dd55a839f11fea2f20ef 100644 --- a/dev/tests/functional/tests/app/Magento/Review/Test/Block/ReviewForm.php +++ b/dev/tests/functional/tests/app/Magento/Review/Test/Block/ReviewForm.php @@ -37,14 +37,14 @@ class ReviewForm extends AbstractForm * * @var string */ - protected $rating = './/*[@id="%s_rating_label"]/span'; + protected $rating = './/*[@id="%s_rating_label"]'; /** * Selector for label of rating vote. * * @var string */ - protected $ratingVoteLabel = './div[contains(@class,"vote")]/label[contains(@id,"_%d_label")]'; + protected $ratingVoteLabel = './following-sibling::div[contains(@class,"vote")]/label[contains(@id,"_%d_label")]'; /** * Submit review form. diff --git a/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/CreateProductReviewBackendEntityTest.php b/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/CreateProductReviewBackendEntityTest.php index aeeff3480e4a623c23f8491f19a180f2acbcab48..9130c5285848e2d04f5978403c23ae0b2278ff16 100644 --- a/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/CreateProductReviewBackendEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/CreateProductReviewBackendEntityTest.php @@ -109,13 +109,14 @@ class CreateProductReviewBackendEntityTest extends Injectable { // Precondition: $product = $review->getDataFieldConfig('entity_id')['source']->getEntity(); - $filter = ['id' => $product->getId()]; + $filter = ['sku' => $product->getSku()]; $this->review = $review; // Steps: $this->reviewIndex->open(); $this->reviewIndex->getReviewActions()->addNew(); - $this->reviewEdit->getProductGrid()->searchAndOpen($filter); + $this->reviewEdit->getProductGrid()->search($filter); + $this->reviewEdit->getProductGrid()->openFirstRow(); $this->reviewEdit->getReviewForm()->fill($this->review); $this->reviewEdit->getPageActions()->save(); 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/tests/app/Magento/Store/Test/Fixture/Store/GroupId.php b/dev/tests/functional/tests/app/Magento/Store/Test/Fixture/Store/GroupId.php index 8b508168c674cca0248f7a316b4640e6ac924066..93bf1a9001cbfcc4825afee549e3873d1d623ae9 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/Fixture/Store/GroupId.php +++ b/dev/tests/functional/tests/app/Magento/Store/Test/Fixture/Store/GroupId.php @@ -39,6 +39,9 @@ class GroupId extends DataSource } $this->storeGroup = $storeGroup; $this->data = $storeGroup->getWebsiteId() . "/" . $storeGroup->getName(); + } elseif (isset($data['fixture'])) { + $this->storeGroup = $data['fixture']; + $this->data = $this->storeGroup->getWebsiteId() . "/" . $this->storeGroup->getName(); } } diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/Fixture/StoreGroup/WebsiteId.php b/dev/tests/functional/tests/app/Magento/Store/Test/Fixture/StoreGroup/WebsiteId.php index 9ad26d88d278d37c51338897c153102399662da7..985b4a91c35f8412eb614b1903a5e56ab8a410e1 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/Fixture/StoreGroup/WebsiteId.php +++ b/dev/tests/functional/tests/app/Magento/Store/Test/Fixture/StoreGroup/WebsiteId.php @@ -39,6 +39,9 @@ class WebsiteId extends DataSource } $this->website = $website; $this->data = $website->getName(); + } elseif (isset($data['fixture'])) { + $this->website = $data['fixture']; + $this->data = $this->website->getName(); } } diff --git a/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/DataGrid.php b/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/DataGrid.php index eae09d72a47b9eb635b7415c056616a1abf19576..3fc6f044f88521794f579aa653bacd6d87e44d14 100644 --- a/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/DataGrid.php +++ b/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/DataGrid.php @@ -135,7 +135,7 @@ class DataGrid extends Grid * * @var string */ - protected $sortLink = "//th[contains(@class, '%s')]/span[contains(text(), '%s')]"; + protected $sortLink = './/div[@data-role="grid-wrapper"]//th[contains(@class, "%s")]/span[contains(text(), "%s")]'; /** * Current page input. 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/Catalog/Model/Product/ImageTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ImageTest.php index 688dc42958b0bacd41e99db0b8575a232e651ba2..88fd7797e768fb5fb227a8fc888ad481794d4347 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ImageTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ImageTest.php @@ -46,7 +46,7 @@ class ImageTest extends \PHPUnit_Framework_TestCase public function testGetUrlPlaceholder($model) { $this->assertStringMatchesFormat( - 'http://localhost/pub/static/frontend/%s/Magento_Catalog/images/product/placeholder/image.jpg', + 'http://localhost/pub/static/%s/frontend/%s/Magento_Catalog/images/product/placeholder/image.jpg', $model->getUrl() ); } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index a1762a065ba8814b50964ec13be8a2a319e616e3..db5511bf9d8ff32be83947c691d063bd4d143205 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -14,6 +14,7 @@ */ namespace Magento\CatalogImportExport\Model\Import; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Category; use Magento\Framework\App\Bootstrap; @@ -557,7 +558,7 @@ class ProductTest extends \Magento\TestFramework\Indexer\TestCase /** * @magentoDataIsolation enabled * @magentoDataFixture mediaImportImageFixture - * + * @magentoAppIsolation enabled * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testSaveMediaImage() @@ -732,6 +733,7 @@ class ProductTest extends \Magento\TestFramework\Indexer\TestCase /** * @magentoDataFixture Magento/Catalog/_files/products_with_multiselect_attribute.php * @magentoAppIsolation enabled + * @magentoDbIsolation enabled */ public function testValidateInvalidMultiselectValues() { @@ -1268,4 +1270,62 @@ class ProductTest extends \Magento\TestFramework\Indexer\TestCase $this->assertEquals($manageStockUseConfig, $stockItem->getUseConfigManageStock()); } } + + /** + * @magentoDataFixture Magento/Store/_files/website.php + * @magentoDataFixture Magento/Store/_files/core_fixturestore.php + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testProductWithMultipleStoresInDifferentBunches() + { + $products = [ + 'simple1', + 'simple2', + 'simple3' + ]; + + $importExportData = $this->getMockBuilder(\Magento\ImportExport\Helper\Data::class) + ->disableOriginalConstructor() + ->getMock(); + $importExportData->expects($this->atLeastOnce()) + ->method('getBunchSize') + ->willReturn(1); + $this->_model = $this->objectManager->create( + \Magento\CatalogImportExport\Model\Import\Product::class, + ['importExportData' => $importExportData] + ); + + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = $this->objectManager->create( + \Magento\ImportExport\Model\Import\Source\Csv::class, + [ + 'file' => __DIR__ . '/_files/products_to_import_with_multiple_store.csv', + 'directory' => $directory + ] + ); + $errors = $this->_model->setParameters( + ['behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, 'entity' => 'catalog_product'] + )->setSource( + $source + )->validateData(); + + $this->assertTrue($errors->getErrorsCount() == 0); + + $this->_model->importData(); + $productCollection = $this->objectManager + ->create(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); + $this->assertCount(3, $productCollection->getItems()); + $actualProductSkus = array_map( + function(ProductInterface $item) { + return $item->getSku(); + }, + $productCollection->getItems() + ); + $this->assertEquals( + $products, + array_values($actualProductSkus) + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_multiple_store.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_multiple_store.csv new file mode 100644 index 0000000000000000000000000000000000000000..a4ad5adb7b0f4a5de7f0371c1002d0a305d07221 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_multiple_store.csv @@ -0,0 +1,6 @@ +sku,product_type,store_view_code,name,price,attribute_set_code,categories +simple1,simple,fixturestore,"simple 1",25,Default,"Default Category/Category 1" +simple1,simple,,"simple 1",25,Default,"Default Category/Category 1" +simple2,simple,fixturestore,"simple 2",34,Default,"Default Category/Category 1" +simple2,simple,,"simple 2",34,Default,"Default Category/Category 1" +simple3,simple,,"simple 3",58,Default,"Default Category/Category 1" diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/ConfigTest.php index a5f5718d17c1eb05b98b1fbc9b8daae4e6a7bb3b..fac6a3b48b4a6603201d18f551ac6c8baf6daeb3 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/ConfigTest.php @@ -42,7 +42,7 @@ class ConfigTest extends \PHPUnit_Framework_TestCase public function testGetConfigCssUrls() { $config = $this->_model->getConfig(); - $publicPathPattern = 'http://localhost/pub/static/adminhtml/Magento/backend/en_US/mage/%s'; + $publicPathPattern = 'http://localhost/pub/static/%s/adminhtml/Magento/backend/en_US/mage/%s'; $this->assertStringMatchesFormat($publicPathPattern, $config->getPopupCss()); $this->assertStringMatchesFormat($publicPathPattern, $config->getContentCss()); } diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/StorageTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/StorageTest.php index 40a97894fc55c2e840384ea1cd5d35d33450ec90..3211bacc613a4dc0dc768061f0a6810e790ac59b 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/StorageTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/StorageTest.php @@ -58,7 +58,7 @@ class StorageTest extends \PHPUnit_Framework_TestCase $this->assertInstanceOf(\Magento\Framework\DataObject::class, $item); $this->assertStringEndsWith('/1.swf', $item->getUrl()); $this->assertStringMatchesFormat( - 'http://%s/static/adminhtml/%s/%s/Magento_Cms/images/placeholder_thumbnail.jpg', + 'http://%s/static/%s/adminhtml/%s/%s/Magento_Cms/images/placeholder_thumbnail.jpg', $item->getThumbUrl() ); return; diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php b/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php index a6527a6c66936b794b9eb38588e335e9076b8880..be9c2afe32f42cc0da7ebc591fc144cae8ad39e2 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php +++ b/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php @@ -283,6 +283,7 @@ class FilterTest extends \PHPUnit_Framework_TestCase * @magentoComponentsDir Magento/Email/Model/_files/design * @magentoAppIsolation enabled * @magentoDbIsolation enabled + * @magentoConfigFixture default_store dev/static/sign 0 * @dataProvider inlinecssDirectiveDataProvider * * @param string $templateText diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php b/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php index 700346fe0aed17d09c669fdf5f8c27a8c7b922b0..15dca70b27a5e4e14e06a8bac09093ed3ce17533 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php +++ b/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php @@ -122,7 +122,7 @@ class TemplateTest extends \PHPUnit_Framework_TestCase ->getArea(Area::AREA_FRONTEND) ->load(); - $expectedViewUrl = 'static/frontend/Magento/blank/en_US/Magento_Theme/favicon.ico'; + $expectedViewUrl = '/frontend/Magento/blank/en_US/Magento_Theme/favicon.ico'; $this->model->setDesignConfig([ 'area' => 'frontend', 'store' => $this->objectManager->get(\Magento\Store\Model\StoreManagerInterface::class) @@ -578,7 +578,6 @@ class TemplateTest extends \PHPUnit_Framework_TestCase ->getArea(Area::AREA_FRONTEND) ->load(); - $expectedViewUrl = 'static/frontend/Magento/blank/en_US/Magento_Theme/favicon.ico'; $this->model->setTemplateSubject('{{view url="Magento_Theme::favicon.ico"}}'); $this->model->setDesignConfig([ 'area' => 'frontend', @@ -588,10 +587,16 @@ class TemplateTest extends \PHPUnit_Framework_TestCase ]); $this->setNotDefaultThemeForFixtureStore(); - $this->assertStringEndsNotWith($expectedViewUrl, $this->model->getProcessedTemplateSubject([])); + $this->assertStringMatchesFormat( + '%s/frontend/Magento/luma/en_US/Magento_Theme/favicon.ico', + $this->model->getProcessedTemplateSubject([]) + ); $this->setDefaultThemeForFixtureStore(); - $this->assertStringEndsWith($expectedViewUrl, $this->model->getProcessedTemplateSubject([])); + $this->assertStringMatchesFormat( + '%s/frontend/Magento/blank/en_US/Magento_Theme/favicon.ico', + $this->model->getProcessedTemplateSubject([]) + ); } /** @@ -605,7 +610,7 @@ class TemplateTest extends \PHPUnit_Framework_TestCase ->load(); $this->assertStringEndsWith( - 'static/frontend/Magento/luma/en_US/Magento_Email/logo_email.png', + '/frontend/Magento/luma/en_US/Magento_Email/logo_email.png', $this->model->getDefaultEmailLogo() ); } 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/Framework/View/Element/AbstractBlockTest.php b/dev/tests/integration/testsuite/Magento/Framework/View/Element/AbstractBlockTest.php index 6cd132378258860fc875308b8fb85f8cd6d94e6e..1846d25f2db390c33e3180a67471382099adfd7a 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/View/Element/AbstractBlockTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/View/Element/AbstractBlockTest.php @@ -477,7 +477,10 @@ class AbstractBlockTest extends \PHPUnit_Framework_TestCase public function testGetViewFileUrl() { $actualResult = $this->_block->getViewFileUrl('css/styles.css'); - $this->assertStringMatchesFormat('http://localhost/pub/static/frontend/%s/en_US/css/styles.css', $actualResult); + $this->assertStringMatchesFormat( + 'http://localhost/pub/static/%s/frontend/%s/en_US/css/styles.css', + $actualResult + ); } public function testGetModuleName() 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/Review/Controller/ProductTest.php b/dev/tests/integration/testsuite/Magento/Review/Controller/ProductTest.php index 35d83ef500077c32be0a6f3c76370e38389a0386..9c54835d3c51188907802ced500b4d1b146c08c6 100644 --- a/dev/tests/integration/testsuite/Magento/Review/Controller/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Review/Controller/ProductTest.php @@ -18,6 +18,6 @@ class ProductTest extends \Magento\TestFramework\TestCase\AbstractController $this->getRequest()->setParam('id', $product->getId()); $this->dispatch('review/product/listAction'); $result = $this->getResponse()->getBody(); - $this->assertContains("static/frontend/Magento/blank/en_US/Magento_Theme/favicon.ico", $result); + $this->assertContains("/frontend/Magento/blank/en_US/Magento_Theme/favicon.ico", $result); } } 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/dev/tests/integration/testsuite/Magento/Theme/Model/Config/ValidatorTest.php b/dev/tests/integration/testsuite/Magento/Theme/Model/Config/ValidatorTest.php index 16ca335beea7b1ada8928efd20396bce5225a1e6..bb899fa6af598e7550bca75a8b6203bf26a9dd3d 100644 --- a/dev/tests/integration/testsuite/Magento/Theme/Model/Config/ValidatorTest.php +++ b/dev/tests/integration/testsuite/Magento/Theme/Model/Config/ValidatorTest.php @@ -12,7 +12,7 @@ use Magento\Email\Model\Template; */ class ValidatorTest extends \PHPUnit_Framework_TestCase { - const TEMPLATE_CODE = 'fixture'; + const TEMPLATE_CODE = 'email_exception_fixture'; /** * @var \Magento\Theme\Model\Design\Config\Validator @@ -44,7 +44,9 @@ class ValidatorTest extends \PHPUnit_Framework_TestCase $this->templateModel = $objectManager->create(\Magento\Email\Model\Template::class); $this->templateModel->load(self::TEMPLATE_CODE, 'template_code'); - + $this->templateFactoryMock->expects($this->once()) + ->method("create") + ->willReturn($this->templateModel); $this->model = $objectManager->create( \Magento\Theme\Model\Design\Config\Validator::class, [ 'templateFactory' => $this->templateFactoryMock ] @@ -58,9 +60,9 @@ class ValidatorTest extends \PHPUnit_Framework_TestCase */ public function testValidateHasRecursiveReference() { - $this->templateFactoryMock->expects($this->once()) - ->method("create") - ->willReturn($this->templateModel); + if (!$this->templateModel->getId()) { + $this->fail('Cannot load Template model'); + } $fieldConfig = [ 'path' => 'design/email/header_template', @@ -90,7 +92,7 @@ class ValidatorTest extends \PHPUnit_Framework_TestCase ->willReturn([$designElementMock]); $designElementMock->expects($this->any())->method('getFieldConfig')->willReturn($fieldConfig); $designElementMock->expects($this->once())->method('getPath')->willReturn($fieldConfig['path']); - $designElementMock->expects($this->once())->method('getValue')->willReturn(1); + $designElementMock->expects($this->once())->method('getValue')->willReturn($this->templateModel->getId()); $this->model->validate($designConfigMock); } @@ -132,7 +134,7 @@ class ValidatorTest extends \PHPUnit_Framework_TestCase ->willReturn([$designElementMock]); $designElementMock->expects($this->any())->method('getFieldConfig')->willReturn($fieldConfig); $designElementMock->expects($this->once())->method('getPath')->willReturn($fieldConfig['path']); - $designElementMock->expects($this->once())->method('getValue')->willReturn(1); + $designElementMock->expects($this->once())->method('getValue')->willReturn($this->templateModel->getId()); $this->model->validate($designConfigMock); } diff --git a/dev/tests/integration/testsuite/Magento/Variable/Model/Variable/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Variable/Model/Variable/ConfigTest.php index d41bae33c54df10ec53d7ce51055e3d69def77f6..4828cb68190e13d5f1b6d9d40c5a4003477052b3 100644 --- a/dev/tests/integration/testsuite/Magento/Variable/Model/Variable/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Variable/Model/Variable/ConfigTest.php @@ -20,7 +20,7 @@ class ConfigTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Variable\Model\Variable\Config::class ); } @@ -28,8 +28,9 @@ class ConfigTest extends \PHPUnit_Framework_TestCase public function testGetWysiwygJsPluginSrc() { $src = $this->_model->getWysiwygJsPluginSrc(); - $this->assertStringStartsWith('http://localhost/pub/static/adminhtml/Magento/backend/en_US/mage/adminhtml/', - $src); - $this->assertStringEndsWith('editor_plugin.js', $src); + $this->assertStringMatchesFormat( + 'http://localhost/pub/static/%s/adminhtml/Magento/backend/en_US/mage/adminhtml/%s/editor_plugin.js', + $src + ); } } diff --git a/dev/tests/integration/testsuite/Magento/Widget/Model/Widget/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Widget/Model/Widget/ConfigTest.php index 91aca8173d6f3d13a92ec513e618dba8ae71cda0..368aa34604a508aa2cadec46e25f49a04cf556ac 100644 --- a/dev/tests/integration/testsuite/Magento/Widget/Model/Widget/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Widget/Model/Widget/ConfigTest.php @@ -43,8 +43,10 @@ class ConfigTest extends \PHPUnit_Framework_TestCase $this->assertArrayHasKey('widget_window_url', $settings); $jsFilename = $settings['widget_plugin_src']; - $this->assertStringStartsWith('http://localhost/pub/static/adminhtml/Magento/backend/en_US/', $jsFilename); - $this->assertStringEndsWith('editor_plugin.js', $jsFilename); + $this->assertStringMatchesFormat( + 'http://localhost/pub/static/%s/adminhtml/Magento/backend/en_US/%s/editor_plugin.js', + $jsFilename + ); $this->assertInternalType('array', $settings['widget_placeholders']); 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/Layout/etc/elements.xsd b/lib/internal/Magento/Framework/View/Layout/etc/elements.xsd index 4966d7f88ffbc9a0a6dc05996f15a0fafa36f323..401bf2e93f3665365cd45cdbe9ee29d2364dbd3f 100755 --- a/lib/internal/Magento/Framework/View/Layout/etc/elements.xsd +++ b/lib/internal/Magento/Framework/View/Layout/etc/elements.xsd @@ -140,7 +140,7 @@ <xs:simpleType name="elementNameType"> <xs:restriction base="xs:string"> - <xs:pattern value="[a-zA-Z][a-zA-Z\d\-_\.]*"/> + <xs:pattern value="[a-zA-Z0-9][a-zA-Z\d\-_\.]*"/> </xs:restriction> </xs:simpleType> 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/lib/web/mage/adminhtml/wysiwyg/tiny_mce/setup.js b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/setup.js index 94de561da986dc5336c7e1a835fccfe869651518..4964e6bb14e703f3a0a11a84ab1af78c7e9cfe6e 100755 --- a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/setup.js +++ b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/setup.js @@ -136,6 +136,10 @@ define([ } }; + if (!settings.style_formats) { + settings.theme_advanced_buttons1 = settings.theme_advanced_buttons1.replace(',styleselect', ''); + } + // Set the document base URL if (this.config.document_base_url) { settings.document_base_url = this.config.document_base_url; 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; diff --git a/setup/pub/magento/setup/web-configuration.js b/setup/pub/magento/setup/web-configuration.js index 164ce5ba63c419bfb41b30b9ff46f543dd60ad56..03a0fc7845dda90225d3be867f4bf9ddd1621f63 100644 --- a/setup/pub/magento/setup/web-configuration.js +++ b/setup/pub/magento/setup/web-configuration.js @@ -68,12 +68,12 @@ angular.module('web-configuration', ['ngStorage']) $scope.$watch('config.address.base_url', function() { if (angular.equals($scope.config.https.text, '') || angular.isUndefined($scope.config.https.text)) { - $scope.config.https.text = $scope.config.address.base_url.replace('http', 'https'); + $scope.config.https.text = $scope.config.address.base_url.replace('http://', 'https://'); } }); $scope.populateHttps = function() { - $scope.config.https.text = $scope.config.address.base_url.replace('http', 'https'); + $scope.config.https.text = $scope.config.address.base_url.replace('http://', 'https://'); }; $scope.showEncryptKey = function() { diff --git a/setup/src/Magento/Setup/Module/Setup.php b/setup/src/Magento/Setup/Module/Setup.php index 39fa5cbda13adb4c88ed30bc377b26efae26b4de..e805306e26ebf93704bc2ecb2990ead6b64bf09e 100644 --- a/setup/src/Magento/Setup/Module/Setup.php +++ b/setup/src/Magento/Setup/Module/Setup.php @@ -25,7 +25,7 @@ class Setup extends \Magento\Framework\Module\Setup implements SchemaSetupInterf $indexType = '', $connectionName = ResourceConnection::DEFAULT_CONNECTION ) { - return $this->getConnection($connectionName)->getIndexName($tableName, $fields, $indexType); + return $this->getConnection($connectionName)->getIndexName($this->getTable($tableName), $fields, $indexType); } /** @@ -46,7 +46,7 @@ class Setup extends \Magento\Framework\Module\Setup implements SchemaSetupInterf $connectionName = ResourceConnection::DEFAULT_CONNECTION ) { return $this->getConnection($connectionName)->getForeignKeyName( - $priTableName, + $this->getTable($priTableName), $priColumnName, $refTableName, $refColumnName diff --git a/setup/src/Magento/Setup/Test/Unit/Module/SetupTest.php b/setup/src/Magento/Setup/Test/Unit/Module/SetupTest.php index 9a75c0a3d4bf307ae19152a8fe923f3d85594fa3..d97356cb1ff7f7e12d248fa82910a507b617d0b8 100644 --- a/setup/src/Magento/Setup/Test/Unit/Module/SetupTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Module/SetupTest.php @@ -6,7 +6,8 @@ namespace Magento\Setup\Test\Unit\Module; -use \Magento\Setup\Module\Setup; +use Magento\Framework\App\ResourceConnection; +use Magento\Setup\Module\Setup; class SetupTest extends \PHPUnit_Framework_TestCase { @@ -22,19 +23,24 @@ class SetupTest extends \PHPUnit_Framework_TestCase */ private $setup; + /** + * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + private $resourceModelMock; + protected function setUp() { - $resourceModel = $this->getMock(\Magento\Framework\App\ResourceConnection::class, [], [], '', false); + $this->resourceModelMock = $this->getMock(ResourceConnection::class, [], [], '', false); $this->connection = $this->getMockForAbstractClass(\Magento\Framework\DB\Adapter\AdapterInterface::class); - $resourceModel->expects($this->any()) + $this->resourceModelMock->expects($this->any()) ->method('getConnection') ->with(self::CONNECTION_NAME) ->will($this->returnValue($this->connection)); - $resourceModel->expects($this->any()) + $this->resourceModelMock->expects($this->any()) ->method('getConnectionByName') - ->with(\Magento\Framework\App\ResourceConnection::DEFAULT_CONNECTION) + ->with(ResourceConnection::DEFAULT_CONNECTION) ->willReturn($this->connection); - $this->setup = new Setup($resourceModel, self::CONNECTION_NAME); + $this->setup = new Setup($this->resourceModelMock, self::CONNECTION_NAME); } public function testGetIdxName() @@ -44,6 +50,11 @@ class SetupTest extends \PHPUnit_Framework_TestCase $indexType = 'index_type'; $expectedIdxName = 'idxName'; + $this->resourceModelMock->expects($this->once()) + ->method('getTableName') + ->with($tableName) + ->will($this->returnValue($tableName)); + $this->connection->expects($this->once()) ->method('getIndexName') ->with($tableName, $fields, $indexType) @@ -59,6 +70,11 @@ class SetupTest extends \PHPUnit_Framework_TestCase $columnName = 'columnName'; $refColumnName = 'refColumnName'; + $this->resourceModelMock->expects($this->once()) + ->method('getTableName') + ->with($tableName) + ->will($this->returnValue($tableName)); + $this->connection->expects($this->once()) ->method('getForeignKeyName') ->with($tableName, $columnName, $refTable, $refColumnName) diff --git a/setup/view/magento/setup/web-configuration.phtml b/setup/view/magento/setup/web-configuration.phtml index d2097f78527b5adaa69a09d471cb70aa21663468..35aa19484ed53f39bf37ae0e90e7b76c7f5efe87 100644 --- a/setup/view/magento/setup/web-configuration.phtml +++ b/setup/view/magento/setup/web-configuration.phtml @@ -68,13 +68,17 @@ $hints = [ ng-init="config.address.auto_base_url = '<?php echo $this->autoBaseUrl ?>'; fillBaseURL();" ng-blur="addSlash()" ng-change="populateHttps()" + ng-pattern="/^(https?:\/\/).*$/" tooltip-placement="right" tooltip-html-unsafe="<?php echo $hints['base_url']; ?>" tooltip-trigger="focus" tooltip-append-to-body="true" > <div class="error-container"> - <span ng-show="webconfig.base_url.$error.required || webconfig.base_url.$error.url"> + <span ng-show="!webconfig.base_url.valid + || webconfig.base_url.$error.required + || webconfig.base_url.$error.url" + > Please enter a valid base URL path. (ex: http://www.example.com/) </span> </div> @@ -189,10 +193,16 @@ $hints = [ ng-class="{'invalid': webconfig.https.$invalid && webconfig.submitted}" ng-if="config.https.front || config.https.admin" ng-focus="" + ng-pattern="/^(https?:\/\/).*$/" required > <div class="error-container"> - <span ng-show="webconfig.https.$error.required || webconfig.https.$error.url">Please enter a valid HTTPS base URL path. (ex: https://www.example.com/)</span> + <span ng-show="!webconfig.https.$error.valid + || webconfig.https.$error.required + || webconfig.https.$error.url" + > + Please enter a valid HTTPS base URL path. (ex: https://www.example.com/) + </span> </div> </div> </div>