diff --git a/app/code/Magento/Bundle/Model/Product/Type.php b/app/code/Magento/Bundle/Model/Product/Type.php index 4cfdf27fd0e6ab560cf296be35bdb8c5ea8e651e..3e8ae11c4faf362831ead26e549621ea483b9c7c 100644 --- a/app/code/Magento/Bundle/Model/Product/Type.php +++ b/app/code/Magento/Bundle/Model/Product/Type.php @@ -546,6 +546,7 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType $selectionsCollection = $this->_bundleCollection->create(); $selectionsCollection->addAttributeToSelect('status'); $selectionsCollection->addQuantityFilter(); + $selectionsCollection->setFlag('product_children', true); $selectionsCollection->addFilterByRequiredOptions(); $selectionsCollection->setOptionIdsFilter([$option->getId()]); diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/PriceTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/PriceTest.php index 9f7952e7ae8c882a5b747f9fd24855feea7c380e..10ca7335668470db793498273a998b64e34f4c4f 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/Product/PriceTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/PriceTest.php @@ -8,60 +8,67 @@ namespace Magento\Bundle\Test\Unit\Model\Product; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; /** + * Test for Model ProductPrice. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class PriceTest extends \PHPUnit_Framework_TestCase { /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\CatalogRule\Model\ResourceModel\RuleFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $ruleFactoryMock; + private $ruleFactoryMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $localeDateMock; + private $localeDateMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $storeManagerMock; + private $storeManagerMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Customer\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ - protected $customerSessionMock; + private $customerSessionMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManagerMock; + private $eventManagerMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Catalog\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ - protected $catalogHelperMock; + private $catalogHelperMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Store\Model\Store|\PHPUnit_Framework_MockObject_MockObject */ - protected $storeMock; + private $storeMock; /** * @var \Magento\Bundle\Model\Product\Price */ - protected $model; + private $model; /** * @var \Magento\Framework\Pricing\PriceCurrencyInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $priceCurrency; + private $priceCurrency; /** - * @var \Magento\Customer\Api\GroupManagementInterface + * @var \Magento\Customer\Api\GroupManagementInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $groupManagement; + private $groupManagement; + /** + * Set up. + * + * @return void + */ protected function setUp() { $this->ruleFactoryMock = $this->getMock( @@ -90,6 +97,7 @@ class PriceTest extends \PHPUnit_Framework_TestCase false ); $scopeConfig = $this->getMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); + $objectManagerHelper = new ObjectManagerHelper($this); $this->model = $objectManagerHelper->getObject( \Magento\Bundle\Model\Product\Price::class, @@ -109,6 +117,8 @@ class PriceTest extends \PHPUnit_Framework_TestCase } /** + * Test for calculateSpecialPrice(). + * * @param float $finalPrice * @param float $specialPrice * @param int $callsNumber @@ -118,6 +128,7 @@ class PriceTest extends \PHPUnit_Framework_TestCase * @covers \Magento\Bundle\Model\Product\Price::calculateSpecialPrice * @covers \Magento\Bundle\Model\Product\Price::__construct * @dataProvider calculateSpecialPrice + * @return void */ public function testCalculateSpecialPrice($finalPrice, $specialPrice, $callsNumber, $dateInInterval, $expected) { @@ -137,6 +148,8 @@ class PriceTest extends \PHPUnit_Framework_TestCase } /** + * Data provider for calculateSpecialPrice() test. + * * @return array */ public function calculateSpecialPrice() @@ -151,6 +164,11 @@ class PriceTest extends \PHPUnit_Framework_TestCase ]; } + /** + * Test for getTotalBundleItemsPrice() with noCustom options. + * + * @return void + */ public function testGetTotalBundleItemsPriceWithNoCustomOptions() { $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) @@ -165,8 +183,11 @@ class PriceTest extends \PHPUnit_Framework_TestCase } /** + * Test for getTotalBundleItemsPrice() with empty options. + * * @param string|null $value * @dataProvider dataProviderWithEmptyOptions + * @return void */ public function testGetTotalBundleItemsPriceWithEmptyOptions($value) { @@ -194,6 +215,8 @@ class PriceTest extends \PHPUnit_Framework_TestCase } /** + * Data provider for getTotalBundleItemsPrice() with empty options. + * * @return array */ public function dataProviderWithEmptyOptions() @@ -205,6 +228,11 @@ class PriceTest extends \PHPUnit_Framework_TestCase ]; } + /** + * Test for getTotalBundleItemsPrice() with empty options. + * + * @return void + */ public function testGetTotalBundleItemsPriceWithNoItems() { $storeId = 1; @@ -240,9 +268,8 @@ class PriceTest extends \PHPUnit_Framework_TestCase ->method('getStoreId') ->willReturn($storeId); - $dataObjectMock->expects($this->once()) - ->method('getValue') - ->willReturn('a:1:{i:0;s:1:"1";}'); + $customOptionValue = 'a:1:{i:0;s:1:"1";}'; + $dataObjectMock->expects($this->once())->method('getValue')->willReturn($customOptionValue); $productTypeMock->expects($this->once()) ->method('getSelectionsByIds') ->with([1], $productMock) diff --git a/app/code/Magento/Catalog/Api/BasePriceStorageInterface.php b/app/code/Magento/Catalog/Api/BasePriceStorageInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..013ec1f940047f1e2c9ce74f95c6f4eb679e04b5 --- /dev/null +++ b/app/code/Magento/Catalog/Api/BasePriceStorageInterface.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Api; + +/** + * Base prices storage. + * @api + */ +interface BasePriceStorageInterface +{ + /** + * Return product prices. + * + * @param string[] $skus + * @return \Magento\Catalog\Api\Data\BasePriceInterface[] + */ + public function get(array $skus); + + /** + * Add or update product prices. + * + * @param \Magento\Catalog\Api\Data\BasePriceInterface[] $prices + * @return bool Will returned True if updated. + */ + public function update(array $prices); +} diff --git a/app/code/Magento/Catalog/Api/CostStorageInterface.php b/app/code/Magento/Catalog/Api/CostStorageInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..0c9fb4d540d5bcbe58b92ffb3d1e3794556d39a6 --- /dev/null +++ b/app/code/Magento/Catalog/Api/CostStorageInterface.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Api; + +/** + * Product cost storage. + * @api + */ +interface CostStorageInterface +{ + /** + * Return product prices. + * + * @param string[] $skus + * @return \Magento\Catalog\Api\Data\CostInterface[] + */ + public function get(array $skus); + + /** + * Add or update product cost. + * + * @param \Magento\Catalog\Api\Data\CostInterface[] $prices + * @return bool Will returned True if updated. + */ + public function update(array $prices); + + /** + * Delete product cost. + * + * @param string[] $skus + * @return bool Will returned True if deleted. + * @throws \Magento\Framework\Exception\CouldNotDeleteException + */ + public function delete(array $skus); +} diff --git a/app/code/Magento/Catalog/Api/Data/BasePriceInterface.php b/app/code/Magento/Catalog/Api/Data/BasePriceInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..942de4a63abef74484e7169adbac07c9faa79cdf --- /dev/null +++ b/app/code/Magento/Catalog/Api/Data/BasePriceInterface.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Api\Data; + +/** + * Price interface. + * @api + */ +interface BasePriceInterface extends \Magento\Framework\Api\ExtensibleDataInterface +{ + /**#@+ + * Constants + */ + const PRICE = 'price'; + const STORE_ID = 'store_id'; + const SKU = 'sku'; + /**#@-*/ + + /** + * Set price. + * + * @param float $price + * @return $this + */ + public function setPrice($price); + + /** + * Get price. + * + * @return float + */ + public function getPrice(); + + /** + * Set store id. + * + * @param int $storeId + * @return $this + */ + public function setStoreId($storeId); + + /** + * Get store id. + * + * @return int + */ + public function getStoreId(); + + /** + * Set SKU. + * + * @param string $sku + * @return $this + */ + public function setSku($sku); + + /** + * Get SKU. + * + * @return string + */ + public function getSku(); + + /** + * Retrieve existing extension attributes object or create a new one. + * + * @return \Magento\Catalog\Api\Data\BasePriceExtensionInterface|null + */ + public function getExtensionAttributes(); + + /** + * Set an extension attributes object. + * + * @param \Magento\Catalog\Api\Data\BasePriceExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Magento\Catalog\Api\Data\BasePriceExtensionInterface $extensionAttributes + ); +} diff --git a/app/code/Magento/Catalog/Api/Data/CostInterface.php b/app/code/Magento/Catalog/Api/Data/CostInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..c007c81f1d7bf9181215044701b18205322be49b --- /dev/null +++ b/app/code/Magento/Catalog/Api/Data/CostInterface.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Api\Data; + +/** + * Cost interface. + * @api + */ +interface CostInterface extends \Magento\Framework\Api\ExtensibleDataInterface +{ + /**#@+ + * Constants + */ + const COST = 'cost'; + const STORE_ID = 'store_id'; + const SKU = 'sku'; + /**#@-*/ + + /** + * Set cost value. + * + * @param float $cost + * @return $this + */ + public function setCost($cost); + + /** + * Get cost value. + * + * @return float + */ + public function getCost(); + + /** + * Set store id. + * + * @param int $storeId + * @return $this + */ + public function setStoreId($storeId); + + /** + * Get store id. + * + * @return int + */ + public function getStoreId(); + + /** + * Set SKU. + * + * @param string $sku + * @return $this + */ + public function setSku($sku); + + /** + * Get SKU. + * + * @return string + */ + public function getSku(); + + /** + * Retrieve existing extension attributes object or create a new one. + * + * @return \Magento\Catalog\Api\Data\CostExtensionInterface|null + */ + public function getExtensionAttributes(); + + /** + * Set an extension attributes object. + * + * @param \Magento\Catalog\Api\Data\CostExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Magento\Catalog\Api\Data\CostExtensionInterface $extensionAttributes + ); +} diff --git a/app/code/Magento/Catalog/Api/Data/TierPriceInterface.php b/app/code/Magento/Catalog/Api/Data/TierPriceInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..1b708132c0d0ef95f900690e53b6520dac2fa008 --- /dev/null +++ b/app/code/Magento/Catalog/Api/Data/TierPriceInterface.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Api\Data; + +/** + * Tier price interface. + * @api + */ +interface TierPriceInterface extends \Magento\Framework\Api\ExtensibleDataInterface +{ + /**#@+ + * Constants + */ + const PRICE = 'price'; + const PRICE_TYPE = 'price_type'; + const WEBSITE_ID = 'website_id'; + const SKU = 'sku'; + const CUSTOMER_GROUP = 'customer_group'; + const QUANTITY = 'quantity'; + const PRICE_TYPE_FIXED = 'fixed'; + const PRICE_TYPE_DISCOUNT = 'discount'; + /**#@-*/ + + /** + * Set tier price. + * + * @param float $price + * @return $this + */ + public function setPrice($price); + + /** + * Get tier price. + * + * @return float + */ + public function getPrice(); + + /** + * Set tier price type. + * + * @param string $type + * @return $this + */ + public function setPriceType($type); + + /** + * Get tier price type. + * + * @return string + */ + public function getPriceType(); + + /** + * Set website id. + * + * @param int $websiteId + * @return $this + */ + public function setWebsiteId($websiteId); + + /** + * Get website id. + * + * @return int + */ + public function getWebsiteId(); + + /** + * Set SKU. + * + * @param string $sku + * @return $this + */ + public function setSku($sku); + + /** + * Get SKU. + * + * @return string + */ + public function getSku(); + + /** + * Set customer group. + * + * @param string $group + * @return $this + */ + public function setCustomerGroup($group); + + /** + * Get customer group. + * + * @return string + */ + public function getCustomerGroup(); + + /** + * Set quantity. + * + * @param float $quantity + * @return $this + */ + public function setQuantity($quantity); + + /** + * Get quantity. + * + * @return float + */ + public function getQuantity(); + + /** + * Retrieve existing extension attributes object or create a new one. + * + * @return \Magento\Catalog\Api\Data\TierPriceExtensionInterface|null + */ + public function getExtensionAttributes(); + + /** + * Set an extension attributes object. + * + * @param \Magento\Catalog\Api\Data\TierPriceExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Magento\Catalog\Api\Data\TierPriceExtensionInterface $extensionAttributes + ); +} diff --git a/app/code/Magento/Catalog/Api/TierPriceStorageInterface.php b/app/code/Magento/Catalog/Api/TierPriceStorageInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..200cdc1baa411c0ec63b9161f5829c1b29724315 --- /dev/null +++ b/app/code/Magento/Catalog/Api/TierPriceStorageInterface.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Api; + +/** + * Tier prices storage. + * @api + */ +interface TierPriceStorageInterface +{ + /** + * Return product prices. + * + * @param string[] $skus + * @return \Magento\Catalog\Api\Data\TierPriceInterface[] + */ + public function get(array $skus); + + /** + * Add or update product prices. + * + * @param \Magento\Catalog\Api\Data\TierPriceInterface[] $prices + * @return bool Will returned True if updated. + */ + public function update(array $prices); + + /** + * Remove existing tier prices and replace them with the new ones. + * + * @param \Magento\Catalog\Api\Data\TierPriceInterface[] $prices + * @return bool Will returned True if replaced. + */ + public function replace(array $prices); + + /** + * Delete product tier prices. + * + * @param \Magento\Catalog\Api\Data\TierPriceInterface[] $prices + * @return bool Will returned True if deleted. + */ + public function delete(array $prices); +} diff --git a/app/code/Magento/Catalog/Model/Config/Source/Product/Options/Price.php b/app/code/Magento/Catalog/Model/Config/Source/Product/Options/Price.php index 5e518df37db1a574f25fe008510e75742cdffa9c..b994c787bee7aa76f8a4baa3648bcbb2c0e4ee27 100644 --- a/app/code/Magento/Catalog/Model/Config/Source/Product/Options/Price.php +++ b/app/code/Magento/Catalog/Model/Config/Source/Product/Options/Price.php @@ -23,7 +23,7 @@ class Price implements ProductPriceOptionsInterface { return [ ['value' => self::VALUE_FIXED, 'label' => __('Fixed')], - ['value' => self::VALUE_PERCENT, 'label' => __('Discount')], + ['value' => self::VALUE_PERCENT, 'label' => __('Percent')], ]; } } diff --git a/app/code/Magento/Catalog/Model/Config/Source/Product/Options/TierPrice.php b/app/code/Magento/Catalog/Model/Config/Source/Product/Options/TierPrice.php new file mode 100644 index 0000000000000000000000000000000000000000..d630f4890fc95afd894ab88b81aedc552e9bc50c --- /dev/null +++ b/app/code/Magento/Catalog/Model/Config/Source/Product/Options/TierPrice.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Model\Config\Source\Product\Options; + +use Magento\Catalog\Model\Config\Source\ProductPriceOptionsInterface; + +/** + * TierPrice types mode source. + */ +class TierPrice implements ProductPriceOptionsInterface +{ + /** + * {@inheritdoc} + * + * @codeCoverageIgnore + */ + public function toOptionArray() + { + return [ + ['value' => self::VALUE_FIXED, 'label' => __('Fixed')], + ['value' => self::VALUE_PERCENT, 'label' => __('Discount')], + ]; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Price/BasePrice.php b/app/code/Magento/Catalog/Model/Product/Price/BasePrice.php new file mode 100644 index 0000000000000000000000000000000000000000..b7c01141de33bb7202ccad4eff2605bb7bdcbae8 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Price/BasePrice.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\Product\Price; + +use Magento\Catalog\Api\Data\BasePriceInterface; + +/** + * Product Base Price DTO. + */ +class BasePrice extends \Magento\Framework\Model\AbstractExtensibleModel implements BasePriceInterface +{ + /** + * {@inheritdoc} + */ + public function setPrice($price) + { + return $this->setData(self::PRICE, $price); + } + + /** + * {@inheritdoc} + */ + public function getPrice() + { + return $this->getData(self::PRICE); + } + + /** + * {@inheritdoc} + */ + public function setStoreId($storeId) + { + return $this->setData(self::STORE_ID, $storeId); + } + + /** + * {@inheritdoc} + */ + public function getStoreId() + { + return $this->getData(self::STORE_ID); + } + + /** + * {@inheritdoc} + */ + public function setSku($sku) + { + return $this->setData(self::SKU, $sku); + } + + /** + * {@inheritdoc} + */ + public function getSku() + { + return $this->getData(self::SKU); + } + + /** + * {@inheritdoc} + */ + public function getExtensionAttributes() + { + return $this->_getExtensionAttributes(); + } + + /** + * {@inheritdoc} + */ + public function setExtensionAttributes(\Magento\Catalog\Api\Data\BasePriceExtensionInterface $extensionAttributes) + { + return $this->_setExtensionAttributes($extensionAttributes); + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Price/BasePriceStorage.php b/app/code/Magento/Catalog/Model/Product/Price/BasePriceStorage.php new file mode 100644 index 0000000000000000000000000000000000000000..e69f89f0bb146d62e7cc65d1022a4d25b6738c4f --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Price/BasePriceStorage.php @@ -0,0 +1,227 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\Product\Price; + +/** + * Base prices storage. + */ +class BasePriceStorage implements \Magento\Catalog\Api\BasePriceStorageInterface +{ + /** + * Attribute code. + * + * @var string + */ + private $attributeCode = 'price'; + + /** + * @var PricePersistence + */ + private $pricePersistence; + + /** + * @var \Magento\Catalog\Api\Data\BasePriceInterfaceFactory + */ + private $basePriceInterfaceFactory; + + /** + * @var \Magento\Catalog\Model\ProductIdLocatorInterface + */ + private $productIdLocator; + + /** + * @var \Magento\Store\Api\StoreRepositoryInterface + */ + private $storeRepository; + + /** + * @var \Magento\Catalog\Api\ProductRepositoryInterface + */ + private $productRepository; + + /** + * Price type allowed. + * + * @var int + */ + private $priceTypeAllowed = 1; + + /** + * Allowed product types. + * + * @var array + */ + private $allowedProductTypes = []; + + /** + * @var PricePersistenceFactory + */ + private $pricePersistenceFactory; + + /** + * BasePriceStorage constructor. + * + * @param PricePersistenceFactory $pricePersistenceFactory + * @param \Magento\Catalog\Api\Data\BasePriceInterfaceFactory $basePriceInterfaceFactory + * @param \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator + * @param \Magento\Store\Api\StoreRepositoryInterface $storeRepository + * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + * @param array $allowedProductTypes + */ + public function __construct( + PricePersistenceFactory $pricePersistenceFactory, + \Magento\Catalog\Api\Data\BasePriceInterfaceFactory $basePriceInterfaceFactory, + \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator, + \Magento\Store\Api\StoreRepositoryInterface $storeRepository, + \Magento\Catalog\Api\ProductRepositoryInterface $productRepository, + array $allowedProductTypes = [] + ) { + $this->pricePersistenceFactory = $pricePersistenceFactory; + $this->basePriceInterfaceFactory = $basePriceInterfaceFactory; + $this->productIdLocator = $productIdLocator; + $this->storeRepository = $storeRepository; + $this->productRepository = $productRepository; + $this->allowedProductTypes = $allowedProductTypes; + } + + /** + * {@inheritdoc} + */ + public function get(array $skus) + { + $this->validateSkus($skus); + $rawPrices = $this->getPricePersistence()->get($skus); + $prices = []; + foreach ($rawPrices as $rawPrice) { + $price = $this->basePriceInterfaceFactory->create(); + $sku = $this->getPricePersistence() + ->retrieveSkuById($rawPrice[$this->getPricePersistence()->getEntityLinkField()], $skus); + $price->setSku($sku); + $price->setPrice($rawPrice['value']); + $price->setStoreId($rawPrice['store_id']); + $prices[] = $price; + } + + return $prices; + } + + /** + * {@inheritdoc} + */ + public function update(array $prices) + { + $this->validate($prices); + $formattedPrices = []; + + foreach ($prices as $price) { + $ids = array_keys($this->productIdLocator->retrieveProductIdsBySkus([$price->getSku()])[$price->getSku()]); + foreach ($ids as $id) { + $formattedPrices[] = [ + 'store_id' => $price->getStoreId(), + $this->getPricePersistence()->getEntityLinkField() => $id, + 'value' => $price->getPrice(), + ]; + } + } + + $this->getPricePersistence()->update($formattedPrices); + + return true; + } + + /** + * Get price persistence. + * + * @return PricePersistence + */ + private function getPricePersistence() + { + if (!$this->pricePersistence) { + $this->pricePersistence = $this->pricePersistenceFactory->create(['attributeCode' => $this->attributeCode]); + } + + return $this->pricePersistence; + } + + /** + * Validate SKU, check product types and skip not existing products. + * + * @param array $skus + * @throws \Magento\Framework\Exception\LocalizedException + * @return void + */ + private function validateSkus(array $skus) + { + $idsBySku = $this->productIdLocator->retrieveProductIdsBySkus($skus); + $skuDiff = array_diff($skus, array_keys($idsBySku)); + + foreach ($idsBySku as $sku => $ids) { + foreach ($ids as $type) { + if (!in_array($type, $this->allowedProductTypes) + || ( + $type == \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE + && $this->productRepository->get($sku)->getPriceType() != $this->priceTypeAllowed + ) + ) { + $skuDiff[] = $sku; + break; + } + } + } + + if (!empty($skuDiff)) { + $values = implode(', ', $skuDiff); + $description = count($skuDiff) == 1 + ? __('Requested product doesn\'t exist: %1', $values) + : __('Requested products don\'t exist: %1', $values); + throw new \Magento\Framework\Exception\NoSuchEntityException($description); + } + } + + /** + * Validate that prices have appropriate values. + * + * @param array $prices + * @throws \Magento\Framework\Exception\LocalizedException + * @return void + */ + private function validate(array $prices) + { + $skus = array_unique( + array_map(function ($price) { + if (!$price->getSku()) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'Invalid attribute %fieldName: %fieldValue.', + [ + 'fieldName' => 'sku', + 'fieldValue' => $price->getSku() + ] + ) + ); + } + return $price->getSku(); + }, $prices) + ); + $this->validateSkus($skus); + + foreach ($prices as $price) { + if (null === $price->getPrice() || $price->getPrice() < 0) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'Invalid attribute %fieldName: %fieldValue.', + [ + 'fieldName' => 'Price', + 'fieldValue' => $price->getPrice() + ] + ) + ); + } + $this->storeRepository->getById($price->getStoreId()); + } + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Price/Cost.php b/app/code/Magento/Catalog/Model/Product/Price/Cost.php new file mode 100644 index 0000000000000000000000000000000000000000..8d52c578ea94b431254883d0e815c1f0064c22d2 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Price/Cost.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\Product\Price; + +use Magento\Catalog\Api\Data\CostInterface; + +/** + * Product Cost DTO. + */ +class Cost extends \Magento\Framework\Model\AbstractExtensibleModel implements CostInterface +{ + /** + * {@inheritdoc} + */ + public function setCost($cost) + { + return $this->setData(self::COST, $cost); + } + + /** + * {@inheritdoc} + */ + public function getCost() + { + return $this->getData(self::COST); + } + + /** + * {@inheritdoc} + */ + public function setStoreId($storeId) + { + return $this->setData(self::STORE_ID, $storeId); + } + + /** + * {@inheritdoc} + */ + public function getStoreId() + { + return $this->getData(self::STORE_ID); + } + + /** + * {@inheritdoc} + */ + public function setSku($sku) + { + return $this->setData(self::SKU, $sku); + } + + /** + * {@inheritdoc} + */ + public function getSku() + { + return $this->getData(self::SKU); + } + + /** + * {@inheritdoc} + */ + public function getExtensionAttributes() + { + return $this->_getExtensionAttributes(); + } + + /** + * {@inheritdoc} + */ + public function setExtensionAttributes(\Magento\Catalog\Api\Data\CostExtensionInterface $extensionAttributes) + { + return $this->_setExtensionAttributes($extensionAttributes); + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Price/CostStorage.php b/app/code/Magento/Catalog/Model/Product/Price/CostStorage.php new file mode 100644 index 0000000000000000000000000000000000000000..e7fc682514a3fa3c575f13938a0a86048a0e50fb --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Price/CostStorage.php @@ -0,0 +1,218 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\Product\Price; + +/** + * Product cost storage. + */ +class CostStorage implements \Magento\Catalog\Api\CostStorageInterface +{ + /** + * Attribute code. + * + * @var string + */ + private $attributeCode = 'cost'; + + /** + * @var PricePersistence + */ + private $pricePersistence; + + /** + * @var \Magento\Catalog\Api\Data\CostInterfaceFactory + */ + private $costInterfaceFactory; + + /** + * @var \Magento\Catalog\Model\ProductIdLocatorInterface + */ + private $productIdLocator; + + /** + * Allowed product types. + * + * @var array + */ + private $allowedProductTypes = []; + + /** + * @var PricePersistenceFactory + */ + private $pricePersistenceFactory; + + /** + * @var \Magento\Store\Api\StoreRepositoryInterface + */ + private $storeRepository; + + /** + * CostStorage constructor. + * + * @param PricePersistenceFactory $pricePersistenceFactory + * @param \Magento\Catalog\Api\Data\CostInterfaceFactory $costInterfaceFactory + * @param \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator + * @param \Magento\Store\Api\StoreRepositoryInterface $storeRepository + * @param array $allowedProductTypes + */ + public function __construct( + PricePersistenceFactory $pricePersistenceFactory, + \Magento\Catalog\Api\Data\CostInterfaceFactory $costInterfaceFactory, + \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator, + \Magento\Store\Api\StoreRepositoryInterface $storeRepository, + array $allowedProductTypes = [] + ) { + $this->pricePersistenceFactory = $pricePersistenceFactory; + $this->costInterfaceFactory = $costInterfaceFactory; + $this->productIdLocator = $productIdLocator; + $this->storeRepository = $storeRepository; + $this->allowedProductTypes = $allowedProductTypes; + } + + /** + * {@inheritdoc} + */ + public function get(array $skus) + { + $this->validateSkus($skus); + $rawPrices = $this->getPricePersistence()->get($skus); + $prices = []; + foreach ($rawPrices as $rawPrice) { + $price = $this->costInterfaceFactory->create(); + $sku = $this->getPricePersistence() + ->retrieveSkuById($rawPrice[$this->getPricePersistence()->getEntityLinkField()], $skus); + $price->setSku($sku); + $price->setCost($rawPrice['value']); + $price->setStoreId($rawPrice['store_id']); + $prices[] = $price; + } + + return $prices; + } + + /** + * {@inheritdoc} + */ + public function update(array $prices) + { + $this->validate($prices); + $formattedPrices = []; + + foreach ($prices as $price) { + $ids = array_keys($this->productIdLocator->retrieveProductIdsBySkus([$price->getSku()])[$price->getSku()]); + foreach ($ids as $id) { + $formattedPrices[] = [ + 'store_id' => $price->getStoreId(), + $this->getPricePersistence()->getEntityLinkField() => $id, + 'value' => $price->getCost(), + ]; + } + } + + $this->getPricePersistence()->update($formattedPrices); + + return true; + } + + /** + * {@inheritdoc} + */ + public function delete(array $skus) + { + $this->validateSkus($skus); + $this->getPricePersistence()->delete($skus); + + return true; + } + + /** + * Get price persistence. + * + * @return PricePersistence + */ + private function getPricePersistence() + { + if (!$this->pricePersistence) { + $this->pricePersistence = $this->pricePersistenceFactory->create(['attributeCode' => $this->attributeCode]); + } + + return $this->pricePersistence; + } + + /** + * Validate that prices have appropriate values. + * + * @param array $prices + * @throws \Magento\Framework\Exception\LocalizedException + * @return void + */ + private function validate(array $prices) + { + $skus = array_unique( + array_map(function ($price) { + if (!$price->getSku()) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'Invalid attribute %fieldName: %fieldValue.', + [ + 'fieldName' => 'sku', + 'fieldValue' => $price->getSku() + ] + ) + ); + } + return $price->getSku(); + }, $prices) + ); + $this->validateSkus($skus); + + foreach ($prices as $price) { + if (null === $price->getCost() || $price->getCost() < 0) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'Invalid attribute %fieldName: %fieldValue.', + [ + 'fieldName' => 'Cost', + 'fieldValue' => $price->getCost() + ] + ) + ); + } + $this->storeRepository->getById($price->getStoreId()); + } + } + + /** + * Validate SKU, check product types and skip not existing products. + * + * @param array $skus + * @throws \Magento\Framework\Exception\LocalizedException + * @return void + */ + private function validateSkus(array $skus) + { + $idsBySku = $this->productIdLocator->retrieveProductIdsBySkus($skus); + $skuDiff = array_diff($skus, array_keys($idsBySku)); + + foreach ($idsBySku as $sku => $ids) { + foreach (array_values($ids) as $type) { + if (!in_array($type, $this->allowedProductTypes)) { + $skuDiff[] = $sku; + break; + } + } + } + + if (!empty($skuDiff)) { + $values = implode(', ', $skuDiff); + $description = count($skuDiff) == 1 + ? __('Requested product doesn\'t exist: %1', $values) + : __('Requested products don\'t exist: %1', $values); + throw new \Magento\Framework\Exception\NoSuchEntityException($description); + } + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Price/PricePersistence.php b/app/code/Magento/Catalog/Model/Product/Price/PricePersistence.php new file mode 100644 index 0000000000000000000000000000000000000000..f37fb15cd47e433e4125b0bd51d90d00ae291cf2 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Price/PricePersistence.php @@ -0,0 +1,228 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\Product\Price; + +/** + * Price persistence. + */ +class PricePersistence +{ + /** + * Price storage table. + * + * @var string + */ + private $table = 'catalog_product_entity_decimal'; + + /** + * @var \Magento\Catalog\Model\ResourceModel\Attribute + */ + private $attributeResource; + + /** + * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface + */ + private $attributeRepository; + + /** + * @var \Magento\Catalog\Model\ProductIdLocatorInterface + */ + private $productIdLocator; + + /** + * Metadata pool. + * + * @var \Magento\Framework\EntityManager\MetadataPool + */ + private $metadataPool; + + /** + * Attribute code. + * + * @var string + */ + private $attributeCode; + + /** + * Attribute ID. + * + * @var int + */ + private $attributeId; + + /** + * Items per operation. + * + * @var int + */ + private $itemsPerOperation = 500; + + /** + * PricePersistence constructor. + * + * @param \Magento\Catalog\Model\ResourceModel\Attribute $attributeResource + * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository + * @param \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator + * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param string $attributeCode + */ + public function __construct( + \Magento\Catalog\Model\ResourceModel\Attribute $attributeResource, + \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository, + \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator, + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + $attributeCode = '' + ) { + $this->attributeResource = $attributeResource; + $this->attributeRepository = $attributeRepository; + $this->attributeCode = $attributeCode; + $this->productIdLocator = $productIdLocator; + $this->metadataPool = $metadataPool; + } + + /** + * Get prices by SKUs. + * + * @param array $skus + * @return array + */ + public function get(array $skus) + { + $ids = $this->retrieveAffectedIds($skus); + $select = $this->attributeResource->getConnection() + ->select() + ->from($this->attributeResource->getTable($this->table)); + return $this->attributeResource->getConnection()->fetchAll( + $select->where($this->getEntityLinkField() . ' IN (?)', $ids) + ->where('attribute_id = ?', $this->getAttributeId()) + ); + } + + /** + * Update prices. + * + * @param array $prices + * @return void + * @throws \Magento\Framework\Exception\CouldNotSaveException + */ + public function update(array $prices) + { + array_walk($prices, function (&$price) { + return $price['attribute_id'] = $this->getAttributeId(); + }); + $connection = $this->attributeResource->getConnection(); + $connection->beginTransaction(); + try { + foreach (array_chunk($prices, $this->itemsPerOperation) as $pricesBunch) { + $this->attributeResource->getConnection()->insertOnDuplicate( + $this->attributeResource->getTable($this->table), + $pricesBunch, + ['value'] + ); + } + $connection->commit(); + } catch (\Exception $e) { + $connection->rollBack(); + throw new \Magento\Framework\Exception\CouldNotSaveException( + __('Could not save Prices.'), + $e + ); + } + } + + /** + * Delete product attribute by SKU. + * + * @param array $skus + * @return void + * @throws \Magento\Framework\Exception\CouldNotDeleteException + */ + public function delete(array $skus) + { + $ids = $this->retrieveAffectedIds($skus); + $connection = $this->attributeResource->getConnection(); + $connection->beginTransaction(); + try { + foreach (array_chunk($ids, $this->itemsPerOperation) as $idsBunch) { + $this->attributeResource->getConnection()->delete( + $this->attributeResource->getTable($this->table), + [ + 'attribute_id = ?' => $this->getAttributeId(), + $this->getEntityLinkField() . ' IN (?)' => $idsBunch + ] + ); + } + $connection->commit(); + } catch (\Exception $e) { + $connection->rollBack(); + throw new \Magento\Framework\Exception\CouldNotDeleteException( + __('Could not delete Prices'), + $e + ); + } + } + + /** + * Retrieve SKU by product ID. + * + * @param int $id + * @param array $skus + * @return int|null + */ + public function retrieveSkuById($id, $skus) + { + foreach ($this->productIdLocator->retrieveProductIdsBySkus($skus) as $sku => $ids) { + if (false !== array_key_exists($id, $ids)) { + return $sku; + } + } + + return null; + } + + /** + * Get attribute ID. + * + * @return int + */ + private function getAttributeId() + { + if (!$this->attributeId) { + $this->attributeId = $this->attributeRepository->get($this->attributeCode)->getAttributeId(); + } + + return $this->attributeId; + } + + /** + * Retrieve affected product IDs. + * + * @param array $skus + * @return array + */ + private function retrieveAffectedIds(array $skus) + { + $affectedIds = []; + + foreach ($this->productIdLocator->retrieveProductIdsBySkus($skus) as $productIds) { + $affectedIds = array_merge($affectedIds, array_keys($productIds)); + } + + return array_unique($affectedIds); + } + + /** + * Get link field. + * + * @return string + */ + public function getEntityLinkField() + { + return $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) + ->getLinkField(); + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPrice.php b/app/code/Magento/Catalog/Model/Product/Price/TierPrice.php new file mode 100644 index 0000000000000000000000000000000000000000..c3c30d18fe639df371a6a5b3d51f7f06492be4f5 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Price/TierPrice.php @@ -0,0 +1,127 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\Product\Price; + +use Magento\Catalog\Api\Data\TierPriceInterface; + +/** + * TierPrice DTO. + */ +class TierPrice extends \Magento\Framework\Model\AbstractExtensibleModel implements TierPriceInterface +{ + /** + * {@inheritdoc} + */ + public function setPrice($price) + { + return $this->setData(self::PRICE, $price); + } + + /** + * {@inheritdoc} + */ + public function getPrice() + { + return $this->getData(self::PRICE); + } + + /** + * {@inheritdoc} + */ + public function setPriceType($type) + { + return $this->setData(self::PRICE_TYPE, $type); + } + + /** + * {@inheritdoc} + */ + public function getPriceType() + { + return $this->getData(self::PRICE_TYPE); + } + + /** + * {@inheritdoc} + */ + public function setWebsiteId($websiteId) + { + return $this->setData(self::WEBSITE_ID, $websiteId); + } + + /** + * {@inheritdoc} + */ + public function getWebsiteId() + { + return $this->getData(self::WEBSITE_ID); + } + + /** + * {@inheritdoc} + */ + public function setSku($sku) + { + return $this->setData(self::SKU, $sku); + } + + /** + * {@inheritdoc} + */ + public function getSku() + { + return $this->getData(self::SKU); + } + + /** + * {@inheritdoc} + */ + public function setCustomerGroup($group) + { + return $this->setData(self::CUSTOMER_GROUP, $group); + } + + /** + * {@inheritdoc} + */ + public function getCustomerGroup() + { + return $this->getData(self::CUSTOMER_GROUP); + } + + /** + * {@inheritdoc} + */ + public function setQuantity($quantity) + { + return $this->setData(self::QUANTITY, $quantity); + } + + /** + * {@inheritdoc} + */ + public function getQuantity() + { + return $this->getData(self::QUANTITY); + } + + /** + * {@inheritdoc} + */ + public function getExtensionAttributes() + { + return $this->_getExtensionAttributes(); + } + + /** + * {@inheritdoc} + */ + public function setExtensionAttributes(\Magento\Catalog\Api\Data\TierPriceExtensionInterface $extensionAttributes) + { + return $this->_setExtensionAttributes($extensionAttributes); + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPriceFactory.php b/app/code/Magento/Catalog/Model/Product/Price/TierPriceFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..1e031649ebdcf2830826e57dccb974a51b7efa71 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Price/TierPriceFactory.php @@ -0,0 +1,169 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\Product\Price; + +use Magento\Catalog\Api\Data\TierPriceInterface; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Tier price factory. + */ +class TierPriceFactory +{ + /** + * Tier price factory. + * + * @var \Magento\Catalog\Api\Data\TierPriceInterfaceFactory + */ + private $tierPriceFactory; + + /** + * Tier price persistence. + * + * @var TierPricePersistence + */ + private $tierPricePersistence; + + /** + * Customer group repository. + * + * @var \Magento\Customer\Api\GroupRepositoryInterface + */ + private $customerGroupRepository; + + /** + * All groups value. + * + * @var string + */ + private $allGroupsValue = 'all groups'; + + /** + * All groups ID. + * + * @var int + */ + private $allGroupsId = 1; + + /** + * Customer groups by code. + * + * @var array + */ + private $customerGroupsByCode = []; + + /** + * TierPriceBuilder constructor. + * + * @param \Magento\Catalog\Api\Data\TierPriceInterfaceFactory $tierPriceFactory + * @param TierPricePersistence $tierPricePersistence + * @param \Magento\Customer\Api\GroupRepositoryInterface $customerGroupRepository + * @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder + * @param \Magento\Framework\Api\FilterBuilder $filterBuilder + */ + public function __construct( + \Magento\Catalog\Api\Data\TierPriceInterfaceFactory $tierPriceFactory, + TierPricePersistence $tierPricePersistence, + \Magento\Customer\Api\GroupRepositoryInterface $customerGroupRepository, + \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder, + \Magento\Framework\Api\FilterBuilder $filterBuilder + ) { + $this->tierPriceFactory = $tierPriceFactory; + $this->tierPricePersistence = $tierPricePersistence; + $this->customerGroupRepository = $customerGroupRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->filterBuilder = $filterBuilder; + } + + /** + * Create populated tier price DTO. + * + * @param array $rawPrice + * @param string $sku + * @return \Magento\Catalog\Api\Data\TierPriceInterface + */ + public function create(array $rawPrice, $sku) + { + $price = $this->tierPriceFactory->create(); + $price->setPrice(isset($rawPrice['percentage_value']) ? $rawPrice['percentage_value'] : $rawPrice['value']); + $price->setPriceType( + isset($rawPrice['percentage_value']) + ? TierPriceInterface::PRICE_TYPE_DISCOUNT + : TierPriceInterface::PRICE_TYPE_FIXED + ); + $price->setWebsiteId($rawPrice['website_id']); + $price->setSku($sku); + $price->setCustomerGroup( + $rawPrice['all_groups'] == $this->allGroupsId + ? $this->allGroupsValue + : $this->customerGroupRepository->getById($rawPrice['customer_group_id'])->getCode() + ); + $price->setQuantity($rawPrice['qty']); + + return $price; + } + + /** + * Build tier price skeleton that has DB consistent format. + * + * @param TierPriceInterface $price + * @param int $id + * @return array + */ + public function createSkeleton(TierPriceInterface $price, $id) + { + return [ + $this->tierPricePersistence->getEntityLinkField() => $id, + 'all_groups' => $this->retrievePriceForAllGroupsValue($price), + 'customer_group_id' => $this->retrievePriceForAllGroupsValue($price) === $this->allGroupsId + ? 0 + : $this->retrieveGroupValue(strtolower($price->getCustomerGroup())), + 'qty' => $price->getQuantity(), + 'value' => $price->getPriceType() === TierPriceInterface::PRICE_TYPE_FIXED + ? $price->getPrice() + : 0.00, + 'percentage_value' => $price->getPriceType() === TierPriceInterface::PRICE_TYPE_DISCOUNT + ? $price->getPrice() + : null, + 'website_id' => $price->getWebsiteId() + ]; + } + + /** + * Retrieve price for all groups value. + * + * @param TierPriceInterface $price + * @return int + */ + private function retrievePriceForAllGroupsValue(TierPriceInterface $price) + { + return strcasecmp($price->getCustomerGroup(), $this->allGroupsValue) === 0 ? $this->allGroupsId : 0; + } + + /** + * Retrieve customer group id by code. + * + * @param string $code + * @return int + * @throws NoSuchEntityException + */ + private function retrieveGroupValue($code) + { + if (!isset($this->customerGroupsByCode[$code])) { + $searchCriteria = $this->searchCriteriaBuilder->addFilters( + [ + $this->filterBuilder->setField('customer_group_code')->setValue($code)->create() + ] + ); + $items = $this->customerGroupRepository->getList($searchCriteria->create())->getItems(); + $item = array_shift($items); + $this->customerGroupsByCode[strtolower($item->getCode())] = $item->getId(); + } + + return $this->customerGroupsByCode[$code]; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPricePersistence.php b/app/code/Magento/Catalog/Model/Product/Price/TierPricePersistence.php new file mode 100644 index 0000000000000000000000000000000000000000..01293d0532fbfc82ed0893436baad30e2baa87b8 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Price/TierPricePersistence.php @@ -0,0 +1,166 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\Product\Price; + +/** + * Persists tier prices. + */ +class TierPricePersistence +{ + /** + * Number or items per each operation. + * + * @var int + */ + private $itemsPerOperation = 500; + + /** + * Tier price resource model. + * + * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice + */ + private $tierpriceResource; + + /** + * Metadata pool. + * + * @var \Magento\Framework\EntityManager\MetadataPool + */ + private $metadataPool; + + /** + * TierPricePersister constructor. + * + * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice $tierpriceResource + * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + */ + public function __construct( + \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice $tierpriceResource, + \Magento\Framework\EntityManager\MetadataPool $metadataPool + ) { + $this->tierpriceResource = $tierpriceResource; + $this->metadataPool = $metadataPool; + } + + /** + * Get tier prices by product IDs. + * + * @param array $ids + * @return array + */ + public function get(array $ids) + { + $select = $this->tierpriceResource->getConnection()->select()->from($this->tierpriceResource->getMainTable()); + return $this->tierpriceResource->getConnection()->fetchAll( + $select->where($this->getEntityLinkField() . ' IN (?)', $ids) + ); + } + + /** + * Update tier prices. + * + * @param array $prices + * @return void + * @throws \Magento\Framework\Exception\CouldNotSaveException + */ + public function update(array $prices) + { + $connection = $this->tierpriceResource->getConnection(); + $connection->beginTransaction(); + try { + foreach (array_chunk($prices, $this->itemsPerOperation) as $pricesBunch) { + $this->tierpriceResource->getConnection()->insertOnDuplicate( + $this->tierpriceResource->getMainTable(), + $pricesBunch, + ['value', 'percentage_value'] + ); + } + $connection->commit(); + } catch (\Exception $e) { + $connection->rollBack(); + throw new \Magento\Framework\Exception\CouldNotSaveException( + __('Could not save Tier Prices'), + $e + ); + } + } + + /** + * Replace prices. + * + * @param array $prices + * @param array $ids + * @return void + * @throws \Magento\Framework\Exception\CouldNotSaveException + */ + public function replace(array $prices, array $ids) + { + $connection = $this->tierpriceResource->getConnection(); + $connection->beginTransaction(); + try { + foreach (array_chunk($ids, $this->itemsPerOperation) as $idsBunch) { + $this->tierpriceResource->getConnection()->delete( + $this->tierpriceResource->getMainTable(), + [$this->getEntityLinkField() . ' IN (?)' => $idsBunch] + ); + } + + foreach (array_chunk($prices, $this->itemsPerOperation) as $pricesBunch) { + $this->tierpriceResource->getConnection()->insertMultiple( + $this->tierpriceResource->getMainTable(), + $pricesBunch + ); + } + $connection->commit(); + } catch (\Exception $e) { + $connection->rollBack(); + throw new \Magento\Framework\Exception\CouldNotSaveException( + __('Could not replace Tier Prices'), + $e + ); + } + } + + /** + * Delete tier prices by IDs. + * + * @param array $ids + * @return void + * @throws \Magento\Framework\Exception\CouldNotDeleteException + */ + public function delete(array $ids) + { + $connection = $this->tierpriceResource->getConnection(); + $connection->beginTransaction(); + try { + foreach (array_chunk($ids, $this->itemsPerOperation) as $idsBunch) { + $this->tierpriceResource->getConnection()->delete( + $this->tierpriceResource->getMainTable(), + ['value_id IN (?)' => $idsBunch] + ); + } + $connection->commit(); + } catch (\Exception $e) { + $connection->rollBack(); + throw new \Magento\Framework\Exception\CouldNotDeleteException( + __('Could not delete Tier Prices'), + $e + ); + } + } + + /** + * Get link field. + * + * @return string + */ + public function getEntityLinkField() + { + return $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) + ->getLinkField(); + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php new file mode 100644 index 0000000000000000000000000000000000000000..83262bbfa1cca6432109f17ef537c4a039c64c04 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php @@ -0,0 +1,323 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\Product\Price; + +use Magento\Catalog\Api\Data\TierPriceInterface; + +/** + * Tier price storage. + */ +class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface +{ + /** + * Tier price resource model. + * + * @var TierPricePersistence + */ + private $tierPricePersistence; + + /** + * Tier price validator. + * + * @var \Magento\Catalog\Model\Product\Price\TierPriceValidator + */ + private $tierPriceValidator; + + /** + * Tier price builder. + * + * @var TierPriceFactory + */ + private $tierPriceFactory; + + /** + * Price indexer. + * + * @var \Magento\Catalog\Model\Indexer\Product\Price + */ + private $priceIndexer; + + /** + * Product ID locator. + * + * @var \Magento\Catalog\Model\ProductIdLocatorInterface + */ + private $productIdLocator; + + /** + * Page cache config. + * + * @var \Magento\PageCache\Model\Config + */ + private $config; + + /** + * Cache type list. + * + * @var \Magento\Framework\App\Cache\TypeListInterface + */ + private $typeList; + + /** + * Indexer chunk value. + * + * @var int + */ + private $indexerChunkValue = 500; + + /** + * TierPriceStorage constructor. + * + * @param TierPricePersistence $tierPricePersistence + * @param TierPriceValidator $tierPriceValidator + * @param TierPriceFactory $tierPriceFactory + * @param \Magento\Catalog\Model\Indexer\Product\Price $priceIndexer + * @param \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator + * @param \Magento\PageCache\Model\Config $config + * @param \Magento\Framework\App\Cache\TypeListInterface $typeList + */ + public function __construct( + TierPricePersistence $tierPricePersistence, + TierPriceValidator $tierPriceValidator, + TierPriceFactory $tierPriceFactory, + \Magento\Catalog\Model\Indexer\Product\Price $priceIndexer, + \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator, + \Magento\PageCache\Model\Config $config, + \Magento\Framework\App\Cache\TypeListInterface $typeList + ) { + $this->tierPricePersistence = $tierPricePersistence; + $this->tierPriceValidator = $tierPriceValidator; + $this->tierPriceFactory = $tierPriceFactory; + $this->priceIndexer = $priceIndexer; + $this->productIdLocator = $productIdLocator; + $this->config = $config; + $this->typeList = $typeList; + } + + /** + * {@inheritdoc} + */ + public function get(array $skus) + { + $this->tierPriceValidator->validateSkus($skus); + $ids = $this->retrieveAffectedIds($skus); + $rawPrices = $this->tierPricePersistence->get($ids); + $prices = []; + + foreach ($rawPrices as $rawPrice) { + $sku = $this->retrieveSkuById($rawPrice[$this->tierPricePersistence->getEntityLinkField()], $skus); + $prices[] = $this->tierPriceFactory->create($rawPrice, $sku); + } + + return $prices; + } + + /** + * {@inheritdoc} + */ + public function update(array $prices) + { + $affectedIds = $this->retrieveAffectedProductIdsForPrices($prices); + $skus = array_unique( + array_map(function ($price) { + return $price->getSku(); + }, $prices) + ); + $this->tierPriceValidator->validatePrices($prices, $this->get($skus)); + $formattedPrices = $this->retrieveFormattedPrices($prices); + $this->tierPricePersistence->update($formattedPrices); + $this->reindexPrices($affectedIds); + $this->invalidateFullPageCache(); + + return true; + } + + /** + * {@inheritdoc} + */ + public function replace(array $prices) + { + $this->tierPriceValidator->validatePrices($prices); + $affectedIds = $this->retrieveAffectedProductIdsForPrices($prices); + $formattedPrices = $this->retrieveFormattedPrices($prices); + $this->tierPricePersistence->replace($formattedPrices, $affectedIds); + $this->reindexPrices($affectedIds); + $this->invalidateFullPageCache(); + + return true; + } + + /** + * {@inheritdoc} + */ + public function delete(array $prices) + { + $affectedIds = $this->retrieveAffectedProductIdsForPrices($prices); + $this->tierPriceValidator->validatePrices($prices); + $priceIds = $this->retrieveAffectedPriceIds($prices); + $this->tierPricePersistence->delete($priceIds); + $this->reindexPrices($affectedIds); + $this->invalidateFullPageCache(); + + return true; + } + + /** + * Retrieve formatted prices. + * + * @param array $prices + * @return array + */ + private function retrieveFormattedPrices(array $prices) + { + $formattedPrices = []; + + foreach ($prices as $price) { + $idsBySku = $this->productIdLocator->retrieveProductIdsBySkus([$price->getSku()]); + $ids = array_keys($idsBySku[$price->getSku()]); + foreach ($ids as $id) { + $formattedPrices[] = $this->tierPriceFactory->createSkeleton($price, $id); + } + } + + return $formattedPrices; + } + + /** + * Retrieve affected product IDs for prices. + * + * @param TierPriceInterface[] $prices + * @return array + */ + private function retrieveAffectedProductIdsForPrices(array $prices) + { + $skus = array_unique( + array_map(function ($price) { + return $price->getSku(); + }, $prices) + ); + + return $this->retrieveAffectedIds($skus); + } + + /** + * Retrieve affected product IDs. + * + * @param array $skus + * @return array + */ + private function retrieveAffectedIds(array $skus) + { + $affectedIds = []; + + foreach ($this->productIdLocator->retrieveProductIdsBySkus($skus) as $productId) { + $affectedIds = array_merge($affectedIds, array_keys($productId)); + } + + return array_unique($affectedIds); + } + + /** + * Retrieve affected price IDs. + * + * @param array $prices + * @return array + */ + private function retrieveAffectedPriceIds(array $prices) + { + $affectedIds = $this->retrieveAffectedProductIdsForPrices($prices); + $formattedPrices = $this->retrieveFormattedPrices($prices); + $existingPrices = $this->tierPricePersistence->get($affectedIds); + $priceIds = []; + + foreach ($formattedPrices as $price) { + $priceIds[] = $this->retrievePriceId($price, $existingPrices); + } + + return $priceIds; + } + + /** + * Retrieve price ID. + * + * @param array $price + * @param array $existingPrices + * @return int + */ + private function retrievePriceId(array $price, array $existingPrices) + { + $linkField = $this->tierPricePersistence->getEntityLinkField(); + + foreach ($existingPrices as $existingPrice) { + if ($existingPrice['all_groups'] == $price['all_groups'] + && $existingPrice['customer_group_id'] == $price['customer_group_id'] + && $existingPrice['qty'] == $price['qty'] + && $this->isCorrectPriceValue($existingPrice, $price) + && $existingPrice[$linkField] == $price[$linkField] + ) { + return $existingPrice['value_id']; + } + } + } + + /** + * Check is correct price value + * + * @param array $existingPrice + * @param array $price + * @return bool + */ + private function isCorrectPriceValue(array $existingPrice, array $price) + { + return ($existingPrice['value'] != 0 && $existingPrice['value'] == $price['value']) + || ($existingPrice['percentage_value'] !== null + && $existingPrice['percentage_value'] == $price['percentage_value']); + } + + /** + * Retrieve SKU by product ID. + * + * @param int $id + * @param array $skus + * @return int|null + */ + private function retrieveSkuById($id, $skus) + { + foreach ($this->productIdLocator->retrieveProductIdsBySkus($skus) as $sku => $ids) { + if (false !== array_key_exists($id, $ids)) { + return $sku; + } + } + + return null; + } + + /** + * Invalidate full page cache. + * + * @return void + */ + private function invalidateFullPageCache() + { + if ($this->config->isEnabled()) { + $this->typeList->invalidate('full_page'); + } + } + + /** + * Reindex prices. + * + * @param array $ids + * @return void + */ + private function reindexPrices(array $ids) + { + foreach (array_chunk($ids, $this->indexerChunkValue) as $affectedIds) { + $this->priceIndexer->execute($affectedIds); + } + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPriceValidator.php b/app/code/Magento/Catalog/Model/Product/Price/TierPriceValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..907fd0f66bbdd3d02ff288c141361d56fcfe9694 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Price/TierPriceValidator.php @@ -0,0 +1,351 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\Product\Price; + +use Magento\Catalog\Api\Data\TierPriceInterface; + +/** + * Tier Price Validator. + */ +class TierPriceValidator +{ + /** + * Groups by code cache. + * + * @var array + */ + private $customerGroupsByCode = []; + + /** + * @var TierPricePersistence + */ + private $tierPricePersistence; + + /** + * All groups value. + * + * @var string + */ + private $allGroupsValue = 'all groups'; + + /** + * All websites value. + * + * @var string + */ + private $allWebsitesValue = "0"; + + /** + * Allowed product types. + * + * @var array + */ + private $allowedProductTypes = []; + + /** + * TierPriceValidator constructor. + * + * @param \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator + * @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder + * @param \Magento\Framework\Api\FilterBuilder $filterBuilder + * @param \Magento\Customer\Api\GroupRepositoryInterface $customerGroupRepository + * @param \Magento\Store\Api\WebsiteRepositoryInterface $websiteRepository + * @param TierPricePersistence $tierPricePersistence + * @param array $allowedProductTypes + */ + public function __construct( + \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator, + \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder, + \Magento\Framework\Api\FilterBuilder $filterBuilder, + \Magento\Customer\Api\GroupRepositoryInterface $customerGroupRepository, + \Magento\Store\Api\WebsiteRepositoryInterface $websiteRepository, + TierPricePersistence $tierPricePersistence, + array $allowedProductTypes = [] + ) { + $this->productIdLocator = $productIdLocator; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->filterBuilder = $filterBuilder; + $this->customerGroupRepository = $customerGroupRepository; + $this->websiteRepository = $websiteRepository; + $this->tierPricePersistence = $tierPricePersistence; + $this->allowedProductTypes = $allowedProductTypes; + } + + /** + * Validate SKU. + * + * @param array $skus + * @throws \Magento\Framework\Exception\LocalizedException + * @return void + */ + public function validateSkus(array $skus) + { + $idsBySku = $this->productIdLocator->retrieveProductIdsBySkus($skus); + $skuDiff = array_diff($skus, array_keys($idsBySku)); + + foreach ($idsBySku as $sku => $ids) { + foreach (array_values($ids) as $type) { + if (!in_array($type, $this->allowedProductTypes)) { + $skuDiff[] = $sku; + break; + } + } + } + + if (!empty($skuDiff)) { + $values = implode(', ', $skuDiff); + $description = count($skuDiff) == 1 + ? __('Requested product doesn\'t exist: %1', $values) + : __('Requested products don\'t exist: %1', $values); + throw new \Magento\Framework\Exception\NoSuchEntityException($description); + } + } + + /** + * Validate that prices have appropriate values and are unique. + * + * @param array $prices + * @param array $existingPrices + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function validatePrices(array $prices, array $existingPrices = []) + { + $skus = array_unique( + array_map(function ($price) { + if (!$price->getSku()) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'Invalid attribute %fieldName: %fieldValue.', + [ + 'fieldName' => 'sku', + 'fieldValue' => $price->getSku() + ] + ) + ); + } + return $price->getSku(); + }, $prices) + ); + $this->validateSkus($skus); + $idsBySku = $this->productIdLocator->retrieveProductIdsBySkus($skus); + + $pricesBySku = []; + + foreach ($prices as $price) { + $pricesBySku[$price->getSku()][] = $price; + } + + /** @var TierPriceInterface $price */ + foreach ($prices as $price) { + $this->checkPrice($price); + $this->checkPriceType($price, $idsBySku[$price->getSku()]); + $this->checkQuantity($price); + $this->checkWebsite($price); + if (isset($pricesBySku[$price->getSku()])) { + $this->checkUnique($price, $pricesBySku[$price->getSku()]); + } + $this->checkUnique($price, $existingPrices); + $this->checkGroup($price); + } + } + + /** + * Verify that price value is correct. + * + * @param TierPriceInterface $price + * @throws \Magento\Framework\Exception\LocalizedException + * @return void + */ + private function checkPrice(TierPriceInterface $price) + { + if ( + null === $price->getPrice() + || $price->getPrice() < 0 + || ($price->getPriceType() === TierPriceInterface::PRICE_TYPE_DISCOUNT && $price->getPrice() > 100) + ) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'Invalid attribute %fieldName: %fieldValue.', + [ + 'fieldName' => 'Price', + 'fieldValue' => $price->getPrice() + ] + ) + ); + } + } + + /** + * Verify that price type is correct. + * + * @param TierPriceInterface $price + * @param array $ids + * @throws \Magento\Framework\Exception\LocalizedException + * @return void + */ + private function checkPriceType(TierPriceInterface $price, array $ids) + { + if ( + !in_array( + $price->getPriceType(), + [TierPriceInterface::PRICE_TYPE_FIXED, TierPriceInterface::PRICE_TYPE_DISCOUNT] + ) + || (array_search(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE, $ids) + && $price->getPriceType() !== TierPriceInterface::PRICE_TYPE_DISCOUNT) + ) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'Invalid attribute %fieldName: %fieldValue.', + [ + 'fieldName' => 'Price Type', + 'fieldValue' => $price->getPriceType() + ] + ) + ); + } + } + + /** + * Verify that product quantity is correct. + * + * @param TierPriceInterface $price + * @throws \Magento\Framework\Exception\LocalizedException + * @return void + */ + private function checkQuantity(TierPriceInterface $price) + { + if ($price->getQuantity() < 1) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'Invalid attribute %fieldName: %fieldValue.', + [ + 'fieldName' => 'Quantity', + 'fieldValue' => $price->getQuantity() + ] + ) + ); + } + } + + /** + * Verify that website exists. + * + * @param TierPriceInterface $price + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function checkWebsite(TierPriceInterface $price) + { + try { + $this->websiteRepository->getById($price->getWebsiteId()); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + throw new \Magento\Framework\Exception\NoSuchEntityException( + __( + 'Invalid attribute %fieldName: %fieldValue.', + [ + 'fieldName' => 'website_id', + 'fieldValue' => $price->getWebsiteId() + ] + ) + ); + } + } + + /** + * Check website value is unique. + * + * @param TierPriceInterface $tierPrice + * @param array $prices + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function checkUnique(TierPriceInterface $tierPrice, array $prices) + { + /** @var TierPriceInterface $price */ + foreach ($prices as $price) { + if ( + $price->getSku() === $tierPrice->getSku() + && $price->getCustomerGroup() === $tierPrice->getCustomerGroup() + && $price->getQuantity() == $tierPrice->getQuantity() + && ( + ($price->getWebsiteId() == $this->allWebsitesValue + || $tierPrice->getWebsiteId() == $this->allWebsitesValue) + && $price->getWebsiteId() != $tierPrice->getWebsiteId() + ) + ) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'We found a duplicate website, tier price, customer group and quantity: ' + . '%fieldName1 = %fieldValue1, %fieldName2 = %fieldValue2, %fieldName3 = %fieldValue3.', + [ + 'fieldName1' => 'Customer Group', + 'fieldValue1' => $price->getCustomerGroup(), + 'fieldName2' => 'Website Id', + 'fieldValue2' => $price->getWebsiteId(), + 'fieldName3' => 'Quantity', + 'fieldValue3' => $price->getQuantity() + ] + ) + ); + } + } + } + + /** + * Check customer group exists and has correct value. + * + * @param TierPriceInterface $price + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @return void + */ + private function checkGroup(TierPriceInterface $price) + { + $customerGroup = strtolower($price->getCustomerGroup()); + + if ($customerGroup != $this->allGroupsValue) { + $this->retrieveGroupValue($customerGroup); + } + } + + /** + * Retrieve customer group id by code. + * + * @param string $code + * @return int + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function retrieveGroupValue($code) + { + if (!isset($this->customerGroupsByCode[$code])) { + $searchCriteria = $this->searchCriteriaBuilder->addFilters( + [ + $this->filterBuilder->setField('customer_group_code')->setValue($code)->create() + ] + ); + $items = $this->customerGroupRepository->getList($searchCriteria->create())->getItems(); + $item = array_shift($items); + + if (!$item) { + throw new \Magento\Framework\Exception\NoSuchEntityException( + __( + 'No such entity with %fieldName = %fieldValue.', + [ + 'fieldName' => 'Customer Group', + 'fieldValue' => $code + ] + ) + ); + } + + $this->customerGroupsByCode[strtolower($item->getCode())] = $item->getId(); + } + + return $this->customerGroupsByCode[$code]; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Type/Price.php b/app/code/Magento/Catalog/Model/Product/Type/Price.php index af15e049203f3f6aae27ef2c6ff071d49cdab8cd..13dfdfe6e5cdc457e751348991fbb806e02ccb12 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/Price.php +++ b/app/code/Magento/Catalog/Model/Product/Type/Price.php @@ -414,11 +414,12 @@ class Price $prices = []; foreach ($tierPrices as $price) { $extensionAttributes = $price->getExtensionAttributes(); - $websiteId = $extensionAttributes && $extensionAttributes->getWebsiteId() - ? $extensionAttributes->getWebsiteId() - : $websiteId; + $priceWebsiteId = $websiteId; + if (isset($extensionAttributes) && is_numeric($extensionAttributes->getWebsiteId())) { + $priceWebsiteId = (string)$extensionAttributes->getWebsiteId(); + } $prices[] = [ - 'website_id' => $websiteId, + 'website_id' => $priceWebsiteId, 'cust_group' => $price->getCustomerGroupId(), 'website_price' => $price->getValue(), 'price' => $price->getValue(), diff --git a/app/code/Magento/Catalog/Model/ProductIdLocator.php b/app/code/Magento/Catalog/Model/ProductIdLocator.php new file mode 100644 index 0000000000000000000000000000000000000000..1678ecd23c9262229ab063c2ad5af65abad9b49d --- /dev/null +++ b/app/code/Magento/Catalog/Model/ProductIdLocator.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model; + +/** + * Product ID locator provides all product IDs by SKUs. + * @api + */ +class ProductIdLocator implements \Magento\Catalog\Model\ProductIdLocatorInterface +{ + /** + * Search Criteria builder. + * + * @var \Magento\Framework\Api\SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * Filter builder. + * + * @var \Magento\Framework\Api\FilterBuilder + */ + private $filterBuilder; + + /** + * Metadata pool. + * + * @var \Magento\Framework\EntityManager\MetadataPool + */ + private $metadataPool; + + /** + * Product repository. + * + * @var \Magento\Catalog\Api\ProductRepositoryInterface + */ + private $productRepository; + + /** + * IDs by SKU cache. + * + * @var array + */ + private $idsBySku = []; + + /** + * ProductIdLocatorInterface constructor. + * + * @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder + * @param \Magento\Framework\Api\FilterBuilder $filterBuilder + * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + */ + public function __construct( + \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder, + \Magento\Framework\Api\FilterBuilder $filterBuilder, + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + ) { + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->filterBuilder = $filterBuilder; + $this->metadataPool = $metadataPool; + $this->productRepository = $productRepository; + } + + /** + * {@inheritdoc} + */ + public function retrieveProductIdsBySkus(array $skus) + { + $skus = array_map('trim', $skus); + $skusInCache = $this->idsBySku ? array_keys($this->idsBySku) : []; + $neededSkus = array_diff($skus, $skusInCache); + + if (!empty($neededSkus)) { + $searchCriteria = $this->searchCriteriaBuilder->addFilters( + [ + $this->filterBuilder + ->setField(\Magento\Catalog\Api\Data\ProductInterface::SKU) + ->setConditionType('in') + ->setValue($neededSkus) + ->create(), + ] + ); + $items = $this->productRepository->getList($searchCriteria->create())->getItems(); + $linkField = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) + ->getLinkField(); + + foreach ($items as $item) { + $this->idsBySku[$item->getSku()][$item->getData($linkField)] = $item->getTypeId(); + } + } + + return array_intersect_key($this->idsBySku, array_flip($skus)); + } +} diff --git a/app/code/Magento/Catalog/Model/ProductIdLocatorInterface.php b/app/code/Magento/Catalog/Model/ProductIdLocatorInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..f9a0d88df2eac54e741478d803891f9eb1214152 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ProductIdLocatorInterface.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Model; + +/** + * Product ID locator provides all product IDs by SKU. + */ +interface ProductIdLocatorInterface +{ + /** + * Will return associative array of product ids as key and type as value grouped by SKUs. + * + * @param array $skus + * @return array + */ + public function retrieveProductIdsBySkus(array $skus); +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/BasePriceStorageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/BasePriceStorageTest.php new file mode 100644 index 0000000000000000000000000000000000000000..ef99e550cdc21ad00f9a726d149528af24ce59c5 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/BasePriceStorageTest.php @@ -0,0 +1,326 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Model\Product\Price; + +/** + * Class BasePriceStorageTest. + */ +class BasePriceStorageTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Catalog\Model\Product\Price\PricePersistenceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $pricePersistenceFactory; + + /** + * @var \Magento\Catalog\Model\Product\Price\PricePersistence|\PHPUnit_Framework_MockObject_MockObject + */ + private $pricePersistence; + + /** + * @var \Magento\Catalog\Api\Data\BasePriceInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $basePriceInterfaceFactory; + + /** + * @var \Magento\Catalog\Api\Data\BasePriceInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $basePriceInterface; + + /** + * @var \Magento\Catalog\Model\ProductIdLocatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productIdLocator; + + /** + * @var \Magento\Store\Api\StoreRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeRepository; + + /** + * @var \Magento\Catalog\Api\ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productRepository; + + /** + * @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $product; + + /** + * @var \Magento\Catalog\Model\Product\Price\BasePriceStorage + */ + private $model; + + /** + * Set up. + * + * @return void + */ + protected function setUp() + { + $this->pricePersistenceFactory = $this->getMock( + \Magento\Catalog\Model\Product\Price\PricePersistenceFactory::class, + ['create'], + [], + '', + false + ); + $this->pricePersistence = $this->getMock( + \Magento\Catalog\Model\Product\Price\PricePersistence::class, + ['get', 'retrieveSkuById', 'update', 'getEntityLinkField'], + [], + '', + false + ); + $this->basePriceInterfaceFactory = $this->getMock( + \Magento\Catalog\Api\Data\BasePriceInterfaceFactory::class, + ['create'], + [], + '', + false + ); + $this->basePriceInterface = $this->getMockForAbstractClass( + \Magento\Catalog\Api\Data\BasePriceInterface::class, + [], + '', + false, + true, + true, + ['setSku', 'setPrice', 'setStoreId', 'getSku', 'getPrice', 'getStoreId'] + ); + $this->productIdLocator = $this->getMockForAbstractClass( + \Magento\Catalog\Model\ProductIdLocatorInterface::class, + [], + '', + false, + true, + true, + ['retrieveProductIdsBySkus'] + ); + $this->storeRepository = $this->getMockForAbstractClass( + \Magento\Store\Api\StoreRepositoryInterface::class, + [], + '', + false, + true, + true, + ['getById'] + ); + $this->productRepository = $this->getMockForAbstractClass( + \Magento\Catalog\Api\ProductRepositoryInterface::class, + [], + '', + false, + true, + true, + ['get'] + ); + $this->product = $this->getMockForAbstractClass( + \Magento\Catalog\Api\Data\ProductInterface::class, + [], + '', + false, + true, + true, + ['getPriceType'] + ); + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->model = $objectManager->getObject( + \Magento\Catalog\Model\Product\Price\BasePriceStorage::class, + [ + 'pricePersistenceFactory' => $this->pricePersistenceFactory, + 'basePriceInterfaceFactory' => $this->basePriceInterfaceFactory, + 'productIdLocator' => $this->productIdLocator, + 'storeRepository' => $this->storeRepository, + 'productRepository' => $this->productRepository, + 'allowedProductTypes' => ['simple', 'virtual', 'bundle', 'downloadable'], + ] + ); + } + + /** + * Test get method. + * + * @return void + */ + public function testGet() + { + $skus = ['sku_1', 'sku_2']; + $idsBySku = [ + 'sku_1' => + [ + 1 => \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE + ], + 'sku_2' => + [ + 2 => \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE + ] + ]; + $rawPrices = [ + [ + 'row_id' => 1, + 'value' => 15, + 'store_id' => 1 + ], + [ + 'row_id' => 2, + 'value' => 35, + 'store_id' => 1 + ] + ]; + $this->productIdLocator + ->expects($this->once()) + ->method('retrieveProductIdsBySkus')->with($skus) + ->willReturn($idsBySku); + $this->productRepository->expects($this->once())->method('get')->willReturn($this->product); + $this->product->expects($this->once())->method('getPriceType')->willReturn(1); + $this->pricePersistenceFactory + ->expects($this->once()) + ->method('create') + ->with(['attributeCode' => 'price']) + ->willReturn($this->pricePersistence); + $this->pricePersistence->expects($this->once())->method('get')->with($skus)->willReturn($rawPrices); + $this->pricePersistence->expects($this->atLeastOnce())->method('getEntityLinkField')->willReturn('row_id'); + $this->basePriceInterfaceFactory + ->expects($this->exactly(2)) + ->method('create') + ->willReturn($this->basePriceInterface); + $this->pricePersistence + ->expects($this->exactly(2)) + ->method('retrieveSkuById') + ->willReturnOnConsecutiveCalls('sku_1', 'sku_2'); + $this->basePriceInterface + ->expects($this->exactly(2)) + ->method('setSku') + ->withConsecutive(['sku_1'], ['sku_2']) + ->willReturnSelf(); + $this->basePriceInterface + ->expects($this->exactly(2)) + ->method('setPrice') + ->withConsecutive([15], [35]) + ->willReturnSelf(); + $this->basePriceInterface + ->expects($this->exactly(2)) + ->method('setStoreId') + ->withConsecutive([1], [1]) + ->willReturnSelf(); + $this->model->get($skus); + } + + /** + * Test get method with exception. + * + * @expectedException \Magento\Framework\Exception\NoSuchEntityException + * @expectedExceptionMessage Requested products don't exist: sku_1, sku_2 + */ + public function testGetWithException() + { + $skus = ['sku_1', 'sku_2']; + $idsBySku = [ + 'sku_1' => + [ + 1 => 'configurable' + ], + 'sku_2' => + [ + 2 => 'grouped' + ] + ]; + $this->productIdLocator + ->expects($this->once()) + ->method('retrieveProductIdsBySkus')->with($skus) + ->willReturn($idsBySku); + $this->model->get($skus); + } + + /** + * Test update method. + * + * @return void + */ + public function testUpdate() + { + $store = $this->getMockForAbstractClass( + \Magento\Store\Api\Data\StoreInterface::class, + [], + '', + false + ); + $sku = 'sku_1'; + $idsBySku = [ + 'sku_1' => + [ + 1 => \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE + ] + ]; + $this->basePriceInterface->expects($this->exactly(4))->method('getSku')->willReturn($sku); + $this->productIdLocator + ->expects($this->exactly(2)) + ->method('retrieveProductIdsBySkus')->with([$sku]) + ->willReturn($idsBySku); + $this->productRepository->expects($this->once())->method('get')->willReturn($this->product); + $this->product->expects($this->once())->method('getPriceType')->willReturn(1); + $this->basePriceInterface->expects($this->exactly(3))->method('getPrice')->willReturn(15); + $this->basePriceInterface->expects($this->exactly(2))->method('getStoreId')->willReturn(1); + $this->pricePersistence->expects($this->atLeastOnce())->method('getEntityLinkField')->willReturn('row_id'); + $this->storeRepository->expects($this->once())->method('getById')->with(1)->willReturn($store); + $this->pricePersistenceFactory + ->expects($this->once()) + ->method('create') + ->with(['attributeCode' => 'price']) + ->willReturn($this->pricePersistence); + $formattedPrices = [ + [ + 'store_id' => 1, + 'row_id' => 1, + 'value' => 15 + ] + ]; + $this->pricePersistence->expects($this->once())->method('update')->with($formattedPrices); + $this->assertTrue($this->model->update([$this->basePriceInterface])); + } + + /** + * Test update method without SKU. + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Invalid attribute sku: . + */ + public function testUpdateWithoutSku() + { + $this->basePriceInterface->expects($this->exactly(2))->method('getSku')->willReturn(null); + $this->model->update([$this->basePriceInterface]); + } + + /** + * Test update method with negative price. + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Invalid attribute Price: -15. + */ + public function testUpdateWithNegativePrice() + { + $sku = 'sku_1'; + $idsBySku = [ + 'sku_1' => + [ + 1 => \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE + ] + ]; + $this->basePriceInterface->expects($this->exactly(2))->method('getSku')->willReturn($sku); + $this->productIdLocator + ->expects($this->once(1)) + ->method('retrieveProductIdsBySkus')->with([$sku]) + ->willReturn($idsBySku); + $this->productRepository->expects($this->once())->method('get')->willReturn($this->product); + $this->product->expects($this->once())->method('getPriceType')->willReturn(1); + $this->basePriceInterface->expects($this->exactly(3))->method('getPrice')->willReturn(-15); + $this->model->update([$this->basePriceInterface]); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/CostStorageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/CostStorageTest.php new file mode 100644 index 0000000000000000000000000000000000000000..b1dea66928f1bd98788758420167bde6dcca2329 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/CostStorageTest.php @@ -0,0 +1,322 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Model\Product\Price; + +/** + * Class CostStorageTest. + */ +class CostStorageTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Catalog\Model\Product\Price\PricePersistenceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $pricePersistenceFactory; + + /** + * @var \Magento\Catalog\Model\Product\Price\PricePersistence|\PHPUnit_Framework_MockObject_MockObject + */ + private $pricePersistence; + + /** + * @var \Magento\Catalog\Api\Data\CostInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $costInterfaceFactory; + + /** + * @var \Magento\Catalog\Api\Data\CostInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $costInterface; + + /** + * @var \Magento\Catalog\Model\ProductIdLocatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productIdLocator; + + /** + * @var \Magento\Store\Api\StoreRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeRepository; + + /** + * @var \Magento\Catalog\Model\Product\Price\CostStorage + */ + private $model; + + /** + * Set up. + * + * @return void + */ + protected function setUp() + { + $this->pricePersistenceFactory = $this->getMock( + \Magento\Catalog\Model\Product\Price\PricePersistenceFactory::class, + ['create'], + [], + '', + false + ); + $this->pricePersistence = $this->getMock( + \Magento\Catalog\Model\Product\Price\PricePersistence::class, + ['get', 'retrieveSkuById', 'update', 'delete', 'getEntityLinkField'], + [], + '', + false + ); + $this->costInterfaceFactory = $this->getMock( + \Magento\Catalog\Api\Data\CostInterfaceFactory::class, + ['create'], + [], + '', + false + ); + $this->costInterface = $this->getMockForAbstractClass( + \Magento\Catalog\Api\Data\CostInterface::class, + [], + '', + false, + true, + true, + ['setSku', 'setCost', 'setStoreId', 'getSku', 'getCost', 'getStoreId'] + ); + $this->productIdLocator = $this->getMockForAbstractClass( + \Magento\Catalog\Model\ProductIdLocatorInterface::class, + [], + '', + false, + true, + true, + ['retrieveProductIdsBySkus'] + ); + $this->storeRepository = $this->getMockForAbstractClass( + \Magento\Store\Api\StoreRepositoryInterface::class, + [], + '', + false, + true, + true, + ['getById'] + ); + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->model = $objectManager->getObject( + \Magento\Catalog\Model\Product\Price\CostStorage::class, + [ + 'pricePersistenceFactory' => $this->pricePersistenceFactory, + 'costInterfaceFactory' => $this->costInterfaceFactory, + 'productIdLocator' => $this->productIdLocator, + 'storeRepository' => $this->storeRepository, + 'allowedProductTypes' => ['simple', 'virtual', 'downloadable'], + ] + ); + } + + /** + * Test get method. + * + * @return void + */ + public function testGet() + { + $skus = ['sku_1', 'sku_2']; + $idsBySku = [ + 'sku_1' => + [ + 1 => \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE + ], + 'sku_2' => + [ + 2 => \Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL + ] + ]; + $rawPrices = [ + [ + 'row_id' => 1, + 'value' => 15, + 'store_id' => 1 + ], + [ + 'row_id' => 2, + 'value' => 35, + 'store_id' => 1 + ] + ]; + $this->productIdLocator + ->expects($this->once()) + ->method('retrieveProductIdsBySkus')->with($skus) + ->willReturn($idsBySku); + $this->pricePersistenceFactory + ->expects($this->once()) + ->method('create') + ->with(['attributeCode' => 'cost']) + ->willReturn($this->pricePersistence); + $this->pricePersistence->expects($this->once())->method('get')->with($skus)->willReturn($rawPrices); + $this->costInterfaceFactory + ->expects($this->exactly(2)) + ->method('create') + ->willReturn($this->costInterface); + $this->pricePersistence + ->expects($this->exactly(2)) + ->method('retrieveSkuById') + ->willReturnOnConsecutiveCalls('sku_1', 'sku_2'); + $this->pricePersistence->expects($this->atLeastOnce())->method('getEntityLinkField')->willReturn('row_id'); + $this->costInterface + ->expects($this->exactly(2)) + ->method('setSku') + ->withConsecutive(['sku_1'], ['sku_2']) + ->willReturnSelf(); + $this->costInterface + ->expects($this->exactly(2)) + ->method('setCost') + ->withConsecutive([15], [35]) + ->willReturnSelf(); + $this->costInterface + ->expects($this->exactly(2)) + ->method('setStoreId') + ->withConsecutive([1], [1]) + ->willReturnSelf(); + $this->model->get($skus); + } + + /** + * Test get method with exception. + * + * @expectedException \Magento\Framework\Exception\NoSuchEntityException + * @expectedExceptionMessage Requested products don't exist: sku_1, sku_2 + */ + public function testGetWithException() + { + $skus = ['sku_1', 'sku_2']; + $idsBySku = [ + 'sku_1' => + [ + 1 => 'configurable' + ], + 'sku_2' => + [ + 2 => 'grouped' + ] + ]; + $this->productIdLocator + ->expects($this->once()) + ->method('retrieveProductIdsBySkus')->with($skus) + ->willReturn($idsBySku); + $this->model->get($skus); + } + + /** + * Test update method. + * + * @return void + */ + public function testUpdate() + { + $store = $this->getMockForAbstractClass( + \Magento\Store\Api\Data\StoreInterface::class, + [], + '', + false + ); + $sku = 'sku_1'; + $idsBySku = [ + 'sku_1' => + [ + 1 => \Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL + ] + ]; + $this->costInterface->expects($this->exactly(4))->method('getSku')->willReturn($sku); + $this->productIdLocator + ->expects($this->exactly(2)) + ->method('retrieveProductIdsBySkus')->with([$sku]) + ->willReturn($idsBySku); + $this->costInterface->expects($this->exactly(3))->method('getCost')->willReturn(15); + $this->costInterface->expects($this->exactly(2))->method('getStoreId')->willReturn(1); + $this->pricePersistence->expects($this->atLeastOnce())->method('getEntityLinkField')->willReturn('row_id'); + $this->storeRepository->expects($this->once())->method('getById')->with(1)->willReturn($store); + $this->pricePersistenceFactory + ->expects($this->once()) + ->method('create') + ->with(['attributeCode' => 'cost']) + ->willReturn($this->pricePersistence); + $formattedPrices = [ + [ + 'store_id' => 1, + 'row_id' => 1, + 'value' => 15 + ] + ]; + $this->pricePersistence->expects($this->once())->method('update')->with($formattedPrices); + $this->assertTrue($this->model->update([$this->costInterface])); + } + + /** + * Test update method without SKU. + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Invalid attribute sku: . + */ + public function testUpdateWithoutSku() + { + $this->costInterface->expects($this->exactly(2))->method('getSku')->willReturn(null); + $this->model->update([$this->costInterface]); + } + + /** + * Test update method with negative cost. + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Invalid attribute Cost: -15. + */ + public function testUpdateWithNegativeCost() + { + $sku = 'sku_1'; + $idsBySku = [ + 'sku_1' => + [ + 1 => \Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL + ] + ]; + $this->costInterface->expects($this->exactly(2))->method('getSku')->willReturn($sku); + $this->productIdLocator + ->expects($this->once(1)) + ->method('retrieveProductIdsBySkus')->with([$sku]) + ->willReturn($idsBySku); + $this->costInterface->expects($this->exactly(3))->method('getCost')->willReturn(-15); + $this->model->update([$this->costInterface]); + } + + /** + * Test delete method. + * + * @return void + */ + public function testDelete() + { + $skus = ['sku_1', 'sku_2']; + $idsBySku = [ + 'sku_1' => + [ + 1 => \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE + ], + 'sku_2' => + [ + 2 => \Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL + ] + ]; + $this->productIdLocator + ->expects($this->once()) + ->method('retrieveProductIdsBySkus')->with($skus) + ->willReturn($idsBySku); + $this->pricePersistenceFactory + ->expects($this->once()) + ->method('create') + ->with(['attributeCode' => 'cost']) + ->willReturn($this->pricePersistence); + $this->pricePersistence->expects($this->once())->method('delete')->with($skus); + $this->model->delete($skus); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/PricePersistenceTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/PricePersistenceTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e9f422245d95967905b577c7cc39626007b16d49 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/PricePersistenceTest.php @@ -0,0 +1,402 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Model\Product\Price; + +/** + * Class PricePersistenceTest. + */ +class PricePersistenceTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Catalog\Model\ResourceModel\Attribute|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeResource; + + /** + * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeRepository; + + /** + * @var \Magento\Catalog\Api\Data\ProductAttributeInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productAttribute; + + /** + * @var \Magento\Catalog\Model\ProductIdLocatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productIdLocator; + + /** + * @var \Magento\Framework\DB\Adapter\AdapterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $connection; + + /** + * @var \Magento\Framework\EntityManager\MetadataPool|\PHPUnit_Framework_MockObject_MockObject + */ + private $metadataPool; + + /** + * @var \Magento\Catalog\Model\Product\Price\PricePersistence + */ + private $model; + + /** + * Set up. + * + * @return void + */ + protected function setUp() + { + $this->attributeResource = $this->getMock( + \Magento\Catalog\Model\ResourceModel\Attribute::class, + ['getConnection', 'getTable'], + [], + '', + false + ); + $this->attributeRepository = $this->getMockForAbstractClass( + \Magento\Catalog\Api\ProductAttributeRepositoryInterface::class, + [], + '', + false, + true, + true, + ['get'] + ); + $this->productIdLocator = $this->getMockForAbstractClass( + \Magento\Catalog\Model\ProductIdLocatorInterface::class, + [], + '', + false, + true, + true, + ['retrieveProductIdsBySkus'] + ); + $this->metadataPool = $this->getMock( + \Magento\Framework\EntityManager\MetadataPool::class, + ['getLinkField', 'getMetadata'], + [], + '', + false + ); + $this->connection = $this->getMockForAbstractClass( + \Magento\Framework\DB\Adapter\AdapterInterface::class, + [], + '', + false, + true, + true, + ['select', 'fetchAll', 'beginTransaction', 'insertOnDuplicate', 'commit', 'rollBack', 'delete'] + ); + $this->productAttribute = $this->getMockForAbstractClass( + \Magento\Catalog\Api\Data\ProductAttributeInterface::class, + [], + '', + false, + true, + true, + ['getAttributeId'] + ); + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->model = $objectManager->getObject( + \Magento\Catalog\Model\Product\Price\PricePersistence::class, + [ + 'attributeResource' => $this->attributeResource, + 'attributeRepository' => $this->attributeRepository, + 'productIdLocator' => $this->productIdLocator, + 'metadataPool' => $this->metadataPool, + ] + ); + } + + /** + * Test get method. + * + * @return void + */ + public function testGet() + { + $attributeId = 5; + $skus = ['sku_1', 'sku_2']; + $idsBySku = [ + 'sku_1' => + [ + 1 => \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE + ], + 'sku_2' => + [ + 2 => \Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL + ] + ]; + $select = $this->getMock( + \Magento\Framework\DB\Select::class, + ['from', 'where'], + [], + '', + false + ); + $this->productIdLocator + ->expects($this->once()) + ->method('retrieveProductIdsBySkus')->with($skus) + ->willReturn($idsBySku); + $this->attributeResource->expects($this->exactly(2))->method('getConnection')->willReturn($this->connection); + $this->connection->expects($this->once())->method('select')->willReturn($select); + $this->attributeResource + ->expects($this->once()) + ->method('getTable') + ->with('catalog_product_entity_decimal') + ->willReturn('catalog_product_entity_decimal'); + $select->expects($this->once())->method('from')->with('catalog_product_entity_decimal')->willReturnSelf(); + $this->attributeRepository->expects($this->once())->method('get')->willReturn($this->productAttribute); + $this->productAttribute->expects($this->once())->method('getAttributeId')->willReturn($attributeId); + $select + ->expects($this->exactly(2)) + ->method('where') + ->withConsecutive(['row_id IN (?)', [1, 2]], ['attribute_id = ?', $attributeId]) + ->willReturnSelf(); + $this->metadataPool->expects($this->atLeastOnce())->method('getMetadata')->willReturnSelf(); + $this->metadataPool->expects($this->atLeastOnce())->method('getLinkField')->willReturn('row_id'); + $this->model->get($skus); + } + + /** + * Test update method. + * + * @return void + */ + public function testUpdate() + { + $attributeId = 5; + $prices = [ + [ + 'store_id' => 1, + 'row_id' => 1, + 'value' => 15 + ] + ]; + $this->attributeRepository->expects($this->once())->method('get')->willReturn($this->productAttribute); + $this->productAttribute->expects($this->once())->method('getAttributeId')->willReturn($attributeId); + $this->attributeResource->expects($this->exactly(2))->method('getConnection')->willReturn($this->connection); + $this->connection->expects($this->once())->method('beginTransaction')->willReturnSelf(); + $this->attributeResource + ->expects($this->once()) + ->method('getTable') + ->with('catalog_product_entity_decimal') + ->willReturn('catalog_product_entity_decimal'); + $this->connection + ->expects($this->once()) + ->method('insertOnDuplicate') + ->with( + 'catalog_product_entity_decimal', + [ + [ + 'store_id' => 1, + 'row_id' => 1, + 'value' => 15, + 'attribute_id' => 5, + ] + ], + ['value'] + ) + ->willReturnSelf(); + $this->connection->expects($this->once())->method('commit')->willReturnSelf(); + $this->model->update($prices); + } + + /** + * Test update method throws exception. + * + * @expectedException \Magento\Framework\Exception\CouldNotSaveException + * @expectedExceptionMessage Could not save Prices. + */ + public function testUpdateWithException() + { + $attributeId = 5; + $prices = [ + [ + 'store_id' => 1, + 'row_id' => 1, + 'value' => 15 + ] + ]; + $this->attributeRepository->expects($this->once())->method('get')->willReturn($this->productAttribute); + $this->productAttribute->expects($this->once())->method('getAttributeId')->willReturn($attributeId); + $this->attributeResource->expects($this->exactly(2))->method('getConnection')->willReturn($this->connection); + $this->connection->expects($this->once())->method('beginTransaction')->willReturnSelf(); + $this->attributeResource + ->expects($this->once()) + ->method('getTable') + ->with('catalog_product_entity_decimal') + ->willReturn('catalog_product_entity_decimal'); + $this->connection + ->expects($this->once()) + ->method('insertOnDuplicate') + ->with( + 'catalog_product_entity_decimal', + [ + [ + 'store_id' => 1, + 'row_id' => 1, + 'value' => 15, + 'attribute_id' => 5, + ] + ], + ['value'] + ) + ->willReturnSelf(); + $this->connection->expects($this->once())->method('commit')->willThrowException(new \Exception()); + $this->connection->expects($this->once())->method('rollback')->willReturnSelf(); + $this->model->update($prices); + } + + /** + * Test delete method. + * + * @return void + */ + public function testDelete() + { + $attributeId = 5; + $skus = ['sku_1', 'sku_2']; + $idsBySku = [ + 'sku_1' => + [ + 1 => \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE + ], + 'sku_2' => + [ + 2 => \Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL + ] + ]; + $this->productIdLocator + ->expects($this->once()) + ->method('retrieveProductIdsBySkus')->with($skus) + ->willReturn($idsBySku); + $this->attributeRepository->expects($this->once())->method('get')->willReturn($this->productAttribute); + $this->productAttribute->expects($this->once())->method('getAttributeId')->willReturn($attributeId); + $this->attributeResource->expects($this->exactly(2))->method('getConnection')->willReturn($this->connection); + $this->connection->expects($this->once())->method('beginTransaction')->willReturnSelf(); + $this->attributeResource + ->expects($this->once()) + ->method('getTable') + ->with('catalog_product_entity_decimal') + ->willReturn('catalog_product_entity_decimal'); + $this->connection + ->expects($this->once()) + ->method('delete') + ->with( + 'catalog_product_entity_decimal', + [ + 'attribute_id = ?' => $attributeId, + 'row_id IN (?)' => [1, 2] + ] + ) + ->willReturnSelf(); + $this->connection->expects($this->once())->method('commit')->willReturnSelf(); + $this->metadataPool->expects($this->atLeastOnce())->method('getMetadata')->willReturnSelf(); + $this->metadataPool->expects($this->atLeastOnce())->method('getLinkField')->willReturn('row_id'); + $this->model->delete($skus); + } + + /** + * Test delete method throws exception. + * + * @expectedException \Magento\Framework\Exception\CouldNotDeleteException + * @expectedExceptionMessage Could not delete Prices + */ + public function testDeleteWithException() + { + $attributeId = 5; + $skus = ['sku_1', 'sku_2']; + $idsBySku = [ + 'sku_1' => + [ + 1 => \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE + ], + 'sku_2' => + [ + 2 => \Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL + ] + ]; + $this->productIdLocator + ->expects($this->once()) + ->method('retrieveProductIdsBySkus')->with($skus) + ->willReturn($idsBySku); + $this->attributeRepository->expects($this->once())->method('get')->willReturn($this->productAttribute); + $this->productAttribute->expects($this->once())->method('getAttributeId')->willReturn($attributeId); + $this->attributeResource->expects($this->exactly(2))->method('getConnection')->willReturn($this->connection); + $this->connection->expects($this->once())->method('beginTransaction')->willReturnSelf(); + $this->attributeResource + ->expects($this->once()) + ->method('getTable') + ->with('catalog_product_entity_decimal') + ->willReturn('catalog_product_entity_decimal'); + $this->connection + ->expects($this->once()) + ->method('delete') + ->with( + 'catalog_product_entity_decimal', + [ + 'attribute_id = ?' => $attributeId, + 'row_id IN (?)' => [1, 2] + ] + ) + ->willReturnSelf(); + $this->connection->expects($this->once())->method('commit')->willThrowException(new \Exception()); + $this->connection->expects($this->once())->method('rollBack')->willReturnSelf(); + $this->metadataPool->expects($this->atLeastOnce())->method('getMetadata')->willReturnSelf(); + $this->metadataPool->expects($this->atLeastOnce())->method('getLinkField')->willReturn('row_id'); + $this->model->delete($skus); + } + + /** + * Test retrieveSkuById method. + * + * @param int|null $expectedResult + * @param int $id + * @param array $skus + * @dataProvider dataProviderRetrieveSkuById + */ + public function testRetrieveSkuById($expectedResult, $id, array $skus) + { + $this->productIdLocator + ->expects($this->once()) + ->method('retrieveProductIdsBySkus') + ->willReturn($skus); + + $this->assertEquals($expectedResult, $this->model->retrieveSkuById($id, $skus)); + } + + /** + * Data provider for retrieveSkuById method. + * + * @return array + */ + public function dataProviderRetrieveSkuById() + { + return [ + [ + null, + 2, + ['sku_1' => [1 => 1]] + ], + [ + 'sku_1', + 1, + ['sku_1' => [1 => 1]] + ], + [ + null, + 1, + ['sku_1' => [2 => 1]] + ], + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2a885dd8b83698bffcb0106992bee076692bffca --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php @@ -0,0 +1,292 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Model\Product\Price; + +/** + * TierPriceStorage test. + */ +class TierPriceStorageTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Catalog\Model\Product\Price\TierPricePersistence|\PHPUnit_Framework_MockObject_MockObject + */ + private $tierPricePersistence; + + /** + * @var \Magento\Catalog\Model\Product\Price\TierPriceValidator|\PHPUnit_Framework_MockObject_MockObject + */ + private $tierPriceValidator; + + /** + * @var \Magento\Catalog\Model\Product\Price\TierPriceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $tierPriceFactory; + + /** + * @var \Magento\Catalog\Model\Indexer\Product\Price|\PHPUnit_Framework_MockObject_MockObject + */ + private $priceIndexer; + + /** + * @var \Magento\Catalog\Model\ProductIdLocatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productIdLocator; + + /** + * @var \Magento\PageCache\Model\Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $config; + + /** + * @var \Magento\Framework\App\Cache\TypeListInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $typeList; + + /** + * @var \Magento\Catalog\Model\Product\Price\TierPriceStorage + */ + private $tierPriceStorage; + + /** + * {@inheritdoc} + */ + protected function setUp() + { + $this->tierPricePersistence = $this->getMock( + \Magento\Catalog\Model\Product\Price\TierPricePersistence::class, + [], + [], + '', + false + ); + $this->tierPricePersistence->expects($this->any()) + ->method('getEntityLinkField') + ->willReturn('row_id'); + $this->tierPriceValidator = $this->getMock( + \Magento\Catalog\Model\Product\Price\TierPriceValidator::class, + [], + [], + '', + false + ); + $this->tierPriceFactory = $this->getMock( + \Magento\Catalog\Model\Product\Price\TierPriceFactory::class, + [], + [], + '', + false + ); + $this->priceIndexer = $this->getMock( + \Magento\Catalog\Model\Indexer\Product\Price::class, + [], + [], + '', + false + ); + $this->productIdLocator = $this->getMock( + \Magento\Catalog\Model\ProductIdLocatorInterface::class, + [], + [], + '', + false + ); + $this->config = $this->getMock( + \Magento\PageCache\Model\Config::class, + [], + [], + '', + false + ); + $this->typeList = $this->getMock( + \Magento\Framework\App\Cache\TypeListInterface::class, + [], + [], + '', + false + ); + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->tierPriceStorage = $objectManager->getObject( + \Magento\Catalog\Model\Product\Price\TierPriceStorage::class, + [ + 'tierPricePersistence' => $this->tierPricePersistence, + 'tierPriceValidator' => $this->tierPriceValidator, + 'tierPriceFactory' => $this->tierPriceFactory, + 'priceIndexer' => $this->priceIndexer, + 'productIdLocator' => $this->productIdLocator, + 'config' => $this->config, + 'typeList' => $this->typeList, + ] + ); + } + + /** + * Test get method. + * @return void + */ + public function testGet() + { + $skus = ['simple', 'virtual']; + $this->productIdLocator->expects($this->atLeastOnce()) + ->method('retrieveProductIdsBySkus') + ->with(['simple', 'virtual']) + ->willReturn(['simple' => ['2' => 'simple'], 'virtual' => ['3' => 'virtual']]); + $this->tierPricePersistence->expects($this->once()) + ->method('get') + ->willReturn( + [ + [ + 'value_id' => 1, + 'row_id' => 2, + 'all_groups' => 1, + 'customer_group_id' => 0, + 'qty' => 2.0000, + 'value' => 2.0000, + 'percentage_value' => null, + 'website_id' => 0 + ], + [ + 'value_id' => 2, + 'row_id' => 3, + 'all_groups' => 1, + 'customer_group_id' => 0, + 'qty' => 3.0000, + 'value' => 3.0000, + 'percentage_value' => null, + 'website_id' => 0 + ] + ] + ); + $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\TierPriceInterface::class)->getMockForAbstractClass(); + $this->tierPriceFactory->expects($this->at(0))->method('create')->willReturn($price); + $this->tierPriceFactory->expects($this->at(1))->method('create')->willReturn($price); + $prices = $this->tierPriceStorage->get($skus); + $this->assertNotEmpty($prices); + $this->assertEquals(2, count($prices)); + } + + /** + * Test update method. + * @return void + */ + public function testUpdate() + { + $this->productIdLocator->expects($this->atLeastOnce()) + ->method('retrieveProductIdsBySkus') + ->willReturn(['bundle' => ['2' => 'bundle']]); + $this->tierPriceValidator->expects($this->atLeastOnce())->method('validatePrices')->willReturn(true); + $this->tierPriceFactory->expects($this->atLeastOnce())->method('createSkeleton')->willReturn( + [ + 'row_id' => 2, + 'all_groups' => 1, + 'customer_group_id' => 0, + 'qty' => 2, + 'value' => 3, + 'percentage_value' => null, + 'website_id' => 0 + ] + ); + $this->tierPricePersistence->expects($this->once()) + ->method('get') + ->willReturn( + [ + [ + 'value_id' => 1, + 'row_id' => 2, + 'all_groups' => 1, + 'customer_group_id' => 0, + 'qty' => 2.0000, + 'value' => 2.0000, + 'percentage_value' => null, + 'website_id' => 0 + ] + ] + ); + $this->tierPricePersistence->expects($this->atLeastOnce())->method('update'); + $this->priceIndexer->expects($this->atLeastOnce())->method('execute'); + $this->config->expects($this->atLeastOnce())->method('isEnabled')->willReturn(true); + $this->typeList->expects($this->atLeastOnce())->method('invalidate'); + $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\TierPriceInterface::class)->getMockForAbstractClass(); + $price->method('getSku')->willReturn('bundle'); + $this->assertTrue($this->tierPriceStorage->update([$price])); + } + + /** + * Test replace method. + * @return void + */ + public function testReplace() + { + $this->tierPriceValidator->expects($this->atLeastOnce())->method('validatePrices'); + $this->productIdLocator->expects($this->atLeastOnce()) + ->method('retrieveProductIdsBySkus') + ->willReturn(['virtual' => ['2' => 'virtual']]); + $this->tierPriceFactory->expects($this->atLeastOnce())->method('createSkeleton')->willReturn( + [ + 'row_id' => 3, + 'all_groups' => 1, + 'customer_group_id' => 0, + 'qty' => 3, + 'value' => 7, + 'percentage_value' => null, + 'website_id' => 0 + ] + ); + $this->tierPricePersistence->expects($this->atLeastOnce())->method('replace'); + $this->priceIndexer->expects($this->atLeastOnce())->method('execute'); + $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\TierPriceInterface::class)->getMockForAbstractClass(); + $price->method('getSku')->willReturn('virtual'); + $this->config->expects($this->atLeastOnce())->method('isEnabled')->willReturn(true); + $this->typeList->expects($this->atLeastOnce())->method('invalidate'); + $this->assertTrue($this->tierPriceStorage->replace([$price])); + } + + /** + * Test delete method. + * @return void + */ + public function testDelete() + { + $this->tierPriceValidator->expects($this->atLeastOnce())->method('validatePrices'); + $this->productIdLocator->expects($this->atLeastOnce()) + ->method('retrieveProductIdsBySkus') + ->willReturn(['simple' => ['2' => 'simple']]); + $this->tierPricePersistence->expects($this->once()) + ->method('get') + ->willReturn( + [ + [ + 'value_id' => 7, + 'row_id' => 7, + 'all_groups' => 1, + 'customer_group_id' => 0, + 'qty' => 5.0000, + 'value' => 6.0000, + 'percentage_value' => null, + 'website_id' => 0 + ] + ] + ); + $this->tierPriceFactory->expects($this->atLeastOnce())->method('createSkeleton')->willReturn( + [ + 'row_id' => 3, + 'all_groups' => 1, + 'customer_group_id' => 0, + 'qty' => 3, + 'value' => 7, + 'percentage_value' => null, + 'website_id' => 0 + ] + ); + $this->tierPricePersistence->expects($this->atLeastOnce())->method('delete'); + $this->priceIndexer->expects($this->atLeastOnce())->method('execute'); + $this->config->expects($this->atLeastOnce())->method('isEnabled')->willReturn(true); + $this->typeList->expects($this->atLeastOnce())->method('invalidate'); + $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\TierPriceInterface::class)->getMockForAbstractClass(); + $price->method('getSku')->willReturn('simple'); + $this->assertTrue($this->tierPriceStorage->delete([$price])); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceValidatorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceValidatorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1f44c2a75d1862ded128a2c35622dc148e3403a2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceValidatorTest.php @@ -0,0 +1,471 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Model\Product\Price; + +use Magento\Catalog\Api\Data\TierPriceInterface; + +/** + * Class TierPriceValidatorTest. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class TierPriceValidatorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Catalog\Model\ProductIdLocatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productIdLocator; + + /** + * @var \Magento\Framework\Api\SearchCriteriaBuilder|\PHPUnit_Framework_MockObject_MockObject + */ + private $searchCriteriaBuilder; + + /** + * @var \Magento\Framework\Api\FilterBuilder|\PHPUnit_Framework_MockObject_MockObject + */ + private $filterBuilder; + + /** + * @var \Magento\Customer\Api\GroupRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerGroupRepository; + + /** + * @var \Magento\Store\Api\WebsiteRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $websiteRepository; + + /** + * @var \Magento\Catalog\Model\Product\Price\TierPricePersistence|\PHPUnit_Framework_MockObject_MockObject + */ + private $tierPricePersistence; + + /** + * @var \Magento\Catalog\Api\Data\TierPriceInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $tierPriceInterface; + + /** + * @var \Magento\Catalog\Model\Product\Price\TierPriceValidator + */ + private $model; + + /** + * Set up. + * + * @return void + */ + protected function setUp() + { + $this->productIdLocator = $this->getMockForAbstractClass( + \Magento\Catalog\Model\ProductIdLocatorInterface::class, + [], + '', + false, + true, + true, + ['retrieveProductIdsBySkus'] + ); + $this->searchCriteriaBuilder = $this->getMock( + \Magento\Framework\Api\SearchCriteriaBuilder::class, + ['addFilters', 'create'], + [], + '', + false + ); + $this->filterBuilder = $this->getMock( + \Magento\Framework\Api\FilterBuilder::class, + ['setField', 'setValue', 'create'], + [], + '', + false + ); + $this->customerGroupRepository = $this->getMockForAbstractClass( + \Magento\Customer\Api\GroupRepositoryInterface::class, + [], + '', + false, + true, + true, + ['getList'] + ); + $this->websiteRepository = $this->getMockForAbstractClass( + \Magento\Store\Api\WebsiteRepositoryInterface::class, + [], + '', + false, + true, + true, + ['getById'] + ); + $this->tierPricePersistence = $this->getMock( + \Magento\Catalog\Model\Product\Price\TierPricePersistence::class, + ['addFilters', 'create'], + [], + '', + false + ); + $this->tierPriceInterface = $this->getMockForAbstractClass( + \Magento\Catalog\Api\Data\TierPriceInterface::class, + [], + '', + false, + true, + true, + ['getSku', 'getPrice', 'getPriceType', 'getQuantity', 'getWebsiteId', 'getCustomerGroup'] + ); + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->model = $objectManager->getObject( + \Magento\Catalog\Model\Product\Price\TierPriceValidator::class, + [ + 'productIdLocator' => $this->productIdLocator, + 'searchCriteriaBuilder' => $this->searchCriteriaBuilder, + 'filterBuilder' => $this->filterBuilder, + 'customerGroupRepository' => $this->customerGroupRepository, + 'websiteRepository' => $this->websiteRepository, + 'tierPricePersistence' => $this->tierPricePersistence, + 'allowedProductTypes' => ['simple', 'virtual', 'bundle', 'downloadable'], + ] + ); + } + + /** + * Test validateSkus method. + * + * @return void + */ + public function testValidateSkus() + { + $skus = ['sku_1', 'sku_2']; + $idsBySku = [ + 'sku_1' => [1 => \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE], + 'sku_2' => [2 => \Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL], + ]; + $this->productIdLocator + ->expects($this->once()) + ->method('retrieveProductIdsBySkus') + ->with($skus) + ->willReturn($idsBySku); + $this->model->validateSkus($skus); + } + + /** + * Test validateSkus method throws exception. + * + * @expectedException \Magento\Framework\Exception\NoSuchEntityException + * @expectedExceptionMessage Requested products don't exist: sku_1, sku_2 + */ + public function testValidateSkusWithException() + { + $skus = ['sku_1', 'sku_2']; + $idsBySku = [ + 'sku_1' => [1 => 'grouped'], + 'sku_2' => [2 => 'configurable'], + ]; + $this->productIdLocator + ->expects($this->once()) + ->method('retrieveProductIdsBySkus') + ->with($skus) + ->willReturn($idsBySku); + $this->model->validateSkus($skus); + } + + /** + * Test validatePrices method. + * + * @return void + */ + public function testValidatePrices() + { + $sku = 'sku_1'; + $idsBySku = [ + 'sku_1' => [1 => \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE], + 'sku_2' => [2 => \Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL], + ]; + $productPrice = 15; + $this->tierPriceInterface->expects($this->exactly(8))->method('getSku')->willReturn($sku); + $this->productIdLocator->expects($this->exactly(2))->method('retrieveProductIdsBySkus')->willReturn($idsBySku); + $this->tierPriceInterface->expects($this->exactly(2))->method('getPrice')->willReturn($productPrice); + $this->tierPriceInterface + ->expects($this->exactly(2)) + ->method('getPriceType') + ->willReturn(TierPriceInterface::PRICE_TYPE_FIXED); + $this->tierPriceInterface->expects($this->exactly(3))->method('getQuantity')->willReturn(2); + $this->checkWebsite($this->tierPriceInterface); + $this->checkGroup($this->tierPriceInterface); + $this->model->validatePrices([$this->tierPriceInterface], []); + } + + /** + * Test validatePrices method with downloadable product. + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Invalid attribute sku: . + */ + public function testValidatePricesWithDownloadableProduct() + { + $this->tierPriceInterface->expects($this->exactly(2))->method('getSku')->willReturn(null); + $this->model->validatePrices([$this->tierPriceInterface], []); + } + + /** + * Test validatePrices method with negative price. + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Invalid attribute Price: -15. + */ + public function testValidatePricesWithNegativePrice() + { + $negativePrice = -15; + $sku = 'sku_1'; + $idsBySku = [ + 'sku_1' => [1 => \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE], + 'sku_2' => [2 => \Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL], + ]; + $this->tierPriceInterface->expects($this->exactly(3))->method('getSku')->willReturn($sku); + $this->productIdLocator->expects($this->exactly(2))->method('retrieveProductIdsBySkus')->willReturn($idsBySku); + $this->tierPriceInterface->expects($this->exactly(3))->method('getPrice')->willReturn($negativePrice); + $this->model->validatePrices([$this->tierPriceInterface], []); + } + + /** + * Test validatePrices method with bundle product and fixed price. + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Invalid attribute Price Type: fixed. + */ + public function testValidatePricesWithBundleProductAndFixedPrice() + { + $sku = 'sku_1'; + $idsBySku = [ + 'sku_1' => [1 => \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE], + ]; + $productPrice = 15; + $this->tierPriceInterface->expects($this->exactly(4))->method('getSku')->willReturn($sku); + $this->productIdLocator->expects($this->exactly(2))->method('retrieveProductIdsBySkus')->willReturn($idsBySku); + $this->tierPriceInterface->expects($this->exactly(2))->method('getPrice')->willReturn($productPrice); + $this->tierPriceInterface + ->expects($this->exactly(4)) + ->method('getPriceType') + ->willReturn(TierPriceInterface::PRICE_TYPE_FIXED); + $this->model->validatePrices([$this->tierPriceInterface], []); + } + + /** + * Test validatePrices method with zero quantity. + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Invalid attribute Quantity: 0. + */ + public function testValidatePricesWithZeroQty() + { + $sku = 'sku_1'; + $idsBySku = [ + 'sku_1' => [1 => \Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL], + ]; + $productPrice = 15; + $this->tierPriceInterface->expects($this->exactly(4))->method('getSku')->willReturn($sku); + $this->productIdLocator->expects($this->exactly(2))->method('retrieveProductIdsBySkus')->willReturn($idsBySku); + $this->tierPriceInterface->expects($this->exactly(2))->method('getPrice')->willReturn($productPrice); + $this->tierPriceInterface + ->expects($this->exactly(2)) + ->method('getPriceType') + ->willReturn(TierPriceInterface::PRICE_TYPE_FIXED); + $this->tierPriceInterface->expects($this->exactly(2))->method('getQuantity')->willReturn(0); + $this->model->validatePrices([$this->tierPriceInterface], []); + } + + /** + * Test validatePrices method without website. + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Invalid attribute website_id: 15. + */ + public function testValidatePricesWithoutWebsite() + { + $sku = 'sku_1'; + $idsBySku = [ + 'sku_1' => [1 => \Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL], + ]; + $productPrice = 15; + $exception = new \Magento\Framework\Exception\NoSuchEntityException(); + $this->tierPriceInterface->expects($this->exactly(4))->method('getSku')->willReturn($sku); + $this->productIdLocator->expects($this->exactly(2))->method('retrieveProductIdsBySkus')->willReturn($idsBySku); + $this->tierPriceInterface->expects($this->exactly(2))->method('getPrice')->willReturn($productPrice); + $this->tierPriceInterface + ->expects($this->exactly(2)) + ->method('getPriceType') + ->willReturn(TierPriceInterface::PRICE_TYPE_FIXED); + $this->tierPriceInterface->expects($this->once())->method('getQuantity')->willReturn(2); + $this->websiteRepository->expects($this->once())->method('getById')->willThrowException($exception); + $this->tierPriceInterface->expects($this->exactly(2))->method('getWebsiteId')->willReturn(15); + $this->model->validatePrices([$this->tierPriceInterface], []); + } + + /** + * Test validatePrices method not unique. + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage We found a duplicate website, tier price, customer + * group and quantity: Customer Group = retailer, Website Id = 2, Quantity = 2. + */ + public function testValidatePricesNotUnique() + { + $sku = 'sku_1'; + $idsBySku = [ + 'sku_1' => [1 => \Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL], + ]; + $productPrice = 15; + $this->tierPriceInterface->expects($this->exactly(8))->method('getSku')->willReturn($sku); + $this->productIdLocator->expects($this->exactly(2))->method('retrieveProductIdsBySkus')->willReturn($idsBySku); + $this->tierPriceInterface->expects($this->exactly(2))->method('getPrice')->willReturn($productPrice); + $this->tierPriceInterface + ->expects($this->exactly(2)) + ->method('getPriceType') + ->willReturn(TierPriceInterface::PRICE_TYPE_FIXED); + $website = $this->getMockForAbstractClass( + \Magento\Store\Api\Data\WebsiteInterface::class, + [], + '', + false + ); + $this->tierPriceInterface + ->expects($this->exactly(5)) + ->method('getWebsiteId') + ->willReturnOnConsecutiveCalls(1, 0, 0, 1, 2); + $this->websiteRepository->expects($this->once())->method('getById')->willReturn($website); + $this->tierPriceInterface->expects($this->exactly(4))->method('getQuantity')->willReturn(2); + $this->tierPriceInterface->expects($this->exactly(3))->method('getCustomerGroup')->willReturn('retailer'); + $this->model->validatePrices([$this->tierPriceInterface], []); + } + + /** + * Test validatePrices method without group. + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage No such entity with Customer Group = wholesale. + */ + public function testValidatePricesWithoutGroup() + { + $sku = 'sku_1'; + $idsBySku = [ + 'sku_1' => [1 => \Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL], + ]; + $productPrice = 15; + $this->tierPriceInterface->expects($this->exactly(8))->method('getSku')->willReturn($sku); + $this->productIdLocator->expects($this->exactly(2))->method('retrieveProductIdsBySkus')->willReturn($idsBySku); + $this->tierPriceInterface->expects($this->exactly(2))->method('getPrice')->willReturn($productPrice); + $this->tierPriceInterface + ->expects($this->exactly(2)) + ->method('getPriceType') + ->willReturn(TierPriceInterface::PRICE_TYPE_FIXED); + $this->tierPriceInterface->expects($this->exactly(3))->method('getQuantity')->willReturn(2); + $this->checkWebsite($this->tierPriceInterface); + $searchCriteria = $this->getMock( + \Magento\Framework\Api\SearchCriteria::class, + [], + [], + '', + false + ); + $searchResults = $this->getMockForAbstractClass( + \Magento\Customer\Api\Data\GroupSearchResultsInterface::class, + [], + '', + false, + true, + true, + ['getItems'] + ); + $this->tierPriceInterface->expects($this->exactly(3))->method('getCustomerGroup')->willReturn('wholesale'); + $this->searchCriteriaBuilder->expects($this->once())->method('addFilters')->willReturnSelf(); + $this->filterBuilder->expects($this->once())->method('setField')->with('customer_group_code')->willReturnSelf(); + $this->filterBuilder->expects($this->once())->method('setValue')->with('wholesale')->willReturnSelf(); + $this->filterBuilder->expects($this->once())->method('create')->willReturnSelf(); + $this->searchCriteriaBuilder + ->expects($this->once()) + ->method('create') + ->willReturn($searchCriteria); + $this->customerGroupRepository + ->expects($this->once()) + ->method('getList') + ->with($searchCriteria) + ->willReturn($searchResults); + $searchResults->expects($this->once())->method('getItems')->willReturn([]); + $this->model->validatePrices([$this->tierPriceInterface], []); + } + + /** + * Check website. + * + * @param \PHPUnit_Framework_MockObject_MockObject $price + */ + private function checkWebsite(\PHPUnit_Framework_MockObject_MockObject $price) + { + $website = $this->getMockForAbstractClass( + \Magento\Store\Api\Data\WebsiteInterface::class, + [], + '', + false + ); + $price->expects($this->exactly(3))->method('getWebsiteId')->willReturn(1); + $this->websiteRepository->expects($this->once())->method('getById')->willReturn($website); + } + + /** + * Check group. + * + * @param \PHPUnit_Framework_MockObject_MockObject $price + */ + private function checkGroup(\PHPUnit_Framework_MockObject_MockObject $price) + { + $searchCriteria = $this->getMock( + \Magento\Framework\Api\SearchCriteria::class, + [], + [], + '', + false + ); + $searchResults = $this->getMockForAbstractClass( + \Magento\Customer\Api\Data\GroupSearchResultsInterface::class, + [], + '', + false, + true, + true, + ['getItems'] + ); + $group = $this->getMockForAbstractClass( + \Magento\Customer\Api\Data\GroupInterface::class, + [], + '', + false, + true, + true, + ['getCode', 'getId'] + ); + + $price->expects($this->exactly(3))->method('getCustomerGroup')->willReturn('wholesale'); + $this->searchCriteriaBuilder->expects($this->once())->method('addFilters')->willReturnSelf(); + $this->filterBuilder->expects($this->once())->method('setField')->with('customer_group_code')->willReturnSelf(); + $this->filterBuilder->expects($this->once())->method('setValue')->with('wholesale')->willReturnSelf(); + $this->filterBuilder->expects($this->once())->method('create')->willReturnSelf(); + $this->searchCriteriaBuilder + ->expects($this->once()) + ->method('create') + ->willReturn($searchCriteria); + $this->customerGroupRepository + ->expects($this->once()) + ->method('getList') + ->with($searchCriteria) + ->willReturn($searchResults); + $searchResults->expects($this->once())->method('getItems')->willReturn([$group]); + $group->expects($this->once())->method('getCode')->willReturn('wholesale'); + $group->expects($this->once())->method('getId')->willReturn(4); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductIdLocatorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductIdLocatorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..75f81195f02e420929edbea8580692c1125752b4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductIdLocatorTest.php @@ -0,0 +1,158 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Model; + +/** + * Class ProductIdLocatorTest. + */ +class ProductIdLocatorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Framework\Api\SearchCriteriaBuilder|\PHPUnit_Framework_MockObject_MockObject + */ + private $searchCriteriaBuilder; + + /** + * @var \Magento\Framework\Api\FilterBuilder|\PHPUnit_Framework_MockObject_MockObject + */ + private $filterBuilder; + + /** + * @var \Magento\Framework\EntityManager\MetadataPool|\PHPUnit_Framework_MockObject_MockObject + */ + private $metadataPool; + + /** + * @var \Magento\Catalog\Api\ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productRepository; + + /** + * @var \Magento\Catalog\Model\ProductIdLocator + */ + private $model; + + /** + * Set up. + * + * @return void + */ + protected function setUp() + { + $this->searchCriteriaBuilder = $this->getMock( + \Magento\Framework\Api\SearchCriteriaBuilder::class, + ['addFilters', 'create'], + [], + '', + false + ); + $this->filterBuilder = $this->getMock( + \Magento\Framework\Api\FilterBuilder::class, + ['setField', 'setConditionType', 'setValue', 'create'], + [], + '', + false + ); + $this->metadataPool = $this->getMock( + \Magento\Framework\EntityManager\MetadataPool::class, + ['getMetadata'], + [], + '', + false + ); + $this->productRepository = $this->getMockForAbstractClass( + \Magento\Catalog\Api\ProductRepositoryInterface::class, + [], + '', + false, + true, + true, + ['getList'] + ); + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->model = $objectManager->getObject( + \Magento\Catalog\Model\ProductIdLocator::class, + [ + 'searchCriteriaBuilder' => $this->searchCriteriaBuilder, + 'filterBuilder' => $this->filterBuilder, + 'metadataPool' => $this->metadataPool, + 'productRepository' => $this->productRepository, + ] + ); + } + + /** + * Test retrieve + */ + public function testRetrieveProductIdsBySkus() + { + $skus = ['sku_1', 'sku_2']; + $searchCriteria = $this->getMock( + \Magento\Framework\Api\SearchCriteria::class, + [], + [], + '', + false + ); + $searchResults = $this->getMockForAbstractClass( + \Magento\Catalog\Api\Data\ProductSearchResultsInterface::class, + [], + '', + false, + true, + true, + ['getItems'] + ); + $product = $this->getMockForAbstractClass( + \Magento\Catalog\Api\Data\ProductInterface::class, + [], + '', + false, + true, + true, + ['getSku', 'getData', 'getTypeId'] + ); + $metaDataInterface = $this->getMockForAbstractClass( + \Magento\Framework\EntityManager\EntityMetadataInterface::class, + [], + '', + false, + true, + true, + ['getLinkField'] + ); + $this->searchCriteriaBuilder->expects($this->once())->method('addFilters')->willReturnSelf(); + $this->filterBuilder->expects($this->once())->method('setField')->with('sku')->willReturnSelf(); + $this->filterBuilder->expects($this->once())->method('setConditionType')->with('in')->willReturnSelf(); + $this->filterBuilder->expects($this->once())->method('setValue')->with(['sku_1', 'sku_2'])->willReturnSelf(); + $this->filterBuilder->expects($this->once())->method('create')->willReturnSelf(); + $this->searchCriteriaBuilder + ->expects($this->once()) + ->method('create') + ->willReturn($searchCriteria); + $this->productRepository + ->expects($this->once()) + ->method('getList') + ->with($searchCriteria) + ->willReturn($searchResults); + $searchResults->expects($this->once())->method('getItems')->willReturn([$product]); + $this->metadataPool + ->expects($this->once()) + ->method('getMetadata') + ->with(\Magento\Catalog\Api\Data\ProductInterface::class) + ->willReturn($metaDataInterface); + $metaDataInterface->expects($this->once())->method('getLinkField')->willReturn('entity_id'); + $product->expects($this->once())->method('getSku')->willReturn('sku_1'); + $product->expects($this->once())->method('getData')->with('entity_id')->willReturn(1); + $product->expects($this->once())->method('getTypeId')->willReturn('simple'); + $this->assertEquals( + ['sku_1' => [1 => 'simple']], + $this->model->retrieveProductIdsBySkus($skus) + ); + } +} diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php index 53360de08b434b4a0314fe341760c55fef476ef4..326e92417670a77bc269625153f3a56f715bb7e5 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php @@ -407,6 +407,7 @@ class AdvancedPricing extends AbstractModifier 'data' => [ 'config' => [ 'componentType' => 'dynamicRows', + 'component' => 'Magento_Catalog/js/components/dynamic-rows-tier-price', 'label' => __('Customer Group Price'), 'renderDefaultRecord' => false, 'recordTemplate' => 'record', diff --git a/app/code/Magento/Catalog/etc/adminhtml/di.xml b/app/code/Magento/Catalog/etc/adminhtml/di.xml index 5fc5ec8d44fd0a88f8a5d542d5518fa60ad77629..d6ecaa7c40391de2b12abac33d25b9ecd5620af8 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/di.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/di.xml @@ -169,4 +169,9 @@ <argument name="scopeName" xsi:type="string">product_form.product_form</argument> </arguments> </type> + <type name="Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\TierPrice"> + <arguments> + <argument name="productPriceOptions" xsi:type="object">Magento\Catalog\Model\Config\Source\Product\Options\TierPrice</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index 27b9a19065e99f22fccf4f2f42076c69d0f1e0c2..0142f7c2f261837ae1e0d8f25418486951da3043 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -51,6 +51,13 @@ <preference for="Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface" type="Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolver"/> <preference for="Magento\Catalog\Model\Product\Media\ConfigInterface" type="Magento\Catalog\Model\Product\Media\Config"/> <preference for="Magento\Framework\View\Asset\ContextInterface" type="Magento\Catalog\Model\View\Asset\Image\Context"/> + <preference for="Magento\Catalog\Api\TierPriceStorageInterface" type="Magento\Catalog\Model\Product\Price\TierPriceStorage" /> + <preference for="Magento\Catalog\Api\Data\TierPriceInterface" type="Magento\Catalog\Model\Product\Price\TierPrice" /> + <preference for="Magento\Catalog\Api\BasePriceStorageInterface" type="Magento\Catalog\Model\Product\Price\BasePriceStorage" /> + <preference for="Magento\Catalog\Api\Data\BasePriceInterface" type="Magento\Catalog\Model\Product\Price\BasePrice" /> + <preference for="Magento\Catalog\Api\CostStorageInterface" type="Magento\Catalog\Model\Product\Price\CostStorage" /> + <preference for="Magento\Catalog\Api\Data\CostInterface" type="Magento\Catalog\Model\Product\Price\Cost" /> + <preference for="Magento\Catalog\Model\ProductIdLocatorInterface" type="Magento\Catalog\Model\ProductIdLocator" /> <type name="Magento\Customer\Model\ResourceModel\Visitor"> <plugin name="catalogLog" type="Magento\Catalog\Model\Plugin\Log" /> </type> @@ -848,4 +855,30 @@ </argument> </arguments> </type> + <type name="Magento\Catalog\Model\Product\Price\CostStorage"> + <arguments> + <argument name="allowedProductTypes" xsi:type="array"> + <item name="0" xsi:type="string">simple</item> + <item name="1" xsi:type="string">virtual</item> + </argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Product\Price\BasePriceStorage"> + <arguments> + <argument name="allowedProductTypes" xsi:type="array"> + <item name="0" xsi:type="string">simple</item> + <item name="1" xsi:type="string">virtual</item> + <item name="2" xsi:type="string">bundle</item> + </argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Product\Price\TierPriceValidator"> + <arguments> + <argument name="allowedProductTypes" xsi:type="array"> + <item name="0" xsi:type="string">simple</item> + <item name="1" xsi:type="string">virtual</item> + <item name="2" xsi:type="string">bundle</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Catalog/etc/webapi.xml b/app/code/Magento/Catalog/etc/webapi.xml index 99670c347a89babc45714d5ed0350676f5cf48fe..b53f11b1ff2959a583b1a401a7566ca7eee33589 100644 --- a/app/code/Magento/Catalog/etc/webapi.xml +++ b/app/code/Magento/Catalog/etc/webapi.xml @@ -246,6 +246,60 @@ <resource ref="Magento_Catalog::catalog"/> </resources> </route> + <route url="/V1/products/tier-prices-information" method="POST"> + <service class="Magento\Catalog\Api\TierPriceStorageInterface" method="get"/> + <resources> + <resource ref="Magento_Catalog::catalog"/> + </resources> + </route> + <route url="/V1/products/tier-prices" method="POST"> + <service class="Magento\Catalog\Api\TierPriceStorageInterface" method="update"/> + <resources> + <resource ref="Magento_Catalog::catalog"/> + </resources> + </route> + <route url="/V1/products/tier-prices" method="PUT"> + <service class="Magento\Catalog\Api\TierPriceStorageInterface" method="replace"/> + <resources> + <resource ref="Magento_Catalog::catalog"/> + </resources> + </route> + <route url="/V1/products/tier-prices-delete" method="POST"> + <service class="Magento\Catalog\Api\TierPriceStorageInterface" method="delete"/> + <resources> + <resource ref="Magento_Catalog::catalog"/> + </resources> + </route> + <route url="/V1/products/base-prices-information" method="POST"> + <service class="Magento\Catalog\Api\BasePriceStorageInterface" method="get"/> + <resources> + <resource ref="Magento_Catalog::catalog"/> + </resources> + </route> + <route url="/V1/products/base-prices" method="POST"> + <service class="Magento\Catalog\Api\BasePriceStorageInterface" method="update"/> + <resources> + <resource ref="Magento_Catalog::catalog"/> + </resources> + </route> + <route url="/V1/products/cost-information" method="POST"> + <service class="Magento\Catalog\Api\CostStorageInterface" method="get"/> + <resources> + <resource ref="Magento_Catalog::catalog"/> + </resources> + </route> + <route url="/V1/products/cost" method="POST"> + <service class="Magento\Catalog\Api\CostStorageInterface" method="update"/> + <resources> + <resource ref="Magento_Catalog::catalog"/> + </resources> + </route> + <route url="/V1/products/cost-delete" method="POST"> + <service class="Magento\Catalog\Api\CostStorageInterface" method="delete"/> + <resources> + <resource ref="Magento_Catalog::catalog"/> + </resources> + </route> <route url="/V1/categories/:categoryId" method="DELETE"> <service class="Magento\Catalog\Api\CategoryRepositoryInterface" method="deleteByIdentifier" /> diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/dynamic-rows-tier-price.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/dynamic-rows-tier-price.js new file mode 100644 index 0000000000000000000000000000000000000000..6dc4c747a44514be814ea4d939135070287052d8 --- /dev/null +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/dynamic-rows-tier-price.js @@ -0,0 +1,29 @@ +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'underscore', + 'Magento_Ui/js/dynamic-rows/dynamic-rows' +], function (_, DynamicRows) { + 'use strict'; + + return DynamicRows.extend({ + + /** + * Init header elements + */ + initHeader: function () { + var labels; + + this._super(); + labels = _.clone(this.labels()); + labels = _.sortBy(labels, function (label) { + return label.sortOrder; + }); + + this.labels(labels); + } + }); +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js index fb15e47eaf7d487da1c8671c545f0b99ce2a7445..78bd0ca73c495785f981a6d2d6ef8e4957fe6d9b 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js @@ -242,7 +242,7 @@ define( emailValidationResult = customer.isLoggedIn(); if (!quote.shippingMethod()) { - this.errorValidationMessage($.mage.__('Please specify a shipping method.')); + this.errorValidationMessage($t('Please specify a shipping method.')); return false; } diff --git a/app/code/Magento/Customer/Setup/UpgradeSchema.php b/app/code/Magento/Customer/Setup/UpgradeSchema.php index 33ec2352e865d0b6fd5cbbde5d9646b5dd92975d..46ccd83fcaae11b3ab4720e5ead60b3625bbda38 100755 --- a/app/code/Magento/Customer/Setup/UpgradeSchema.php +++ b/app/code/Magento/Customer/Setup/UpgradeSchema.php @@ -136,14 +136,12 @@ class UpgradeSchema implements UpgradeSchemaInterface ] ); foreach ($keys as $key) { - $setup->getConnection()->modifyColumn( + $description = $setup->getConnection()->describeTable($key['TABLE_NAME'])[$key['COLUMN_NAME']]; + $description['DATA_TYPE'] = 'int'; + $setup->getConnection()->modifyColumnByDdl( $key['TABLE_NAME'], $key['COLUMN_NAME'], - [ - 'type' => 'integer', - 'unsigned' => true, - 'nullable' => false - ] + $description ); } } diff --git a/app/code/Magento/Customer/view/frontend/web/js/view/authentication-popup.js b/app/code/Magento/Customer/view/frontend/web/js/view/authentication-popup.js index 649238a81d1f714cf6f6ac282b2695dec0a36fbf..0dca72ade73b69a4c7603e31286a68162809976f 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/view/authentication-popup.js +++ b/app/code/Magento/Customer/view/frontend/web/js/view/authentication-popup.js @@ -69,19 +69,24 @@ define( }, /** Provide login action */ - login: function (loginForm) { + login: function (formUiElement, event) { var loginData = {}, - formDataArray = $(loginForm).serializeArray(); + formElement = $(event.currentTarget), + formDataArray = formElement.serializeArray(); + + event.stopPropagation(); formDataArray.forEach(function (entry) { loginData[entry.name] = entry.value; }); - if ($(loginForm).validation() && - $(loginForm).validation('isValid') + if (formElement.validation() && + formElement.validation('isValid') ) { this.isLoading(true); - loginAction(loginData, null, false); + loginAction(loginData); } + + return false; } }); } diff --git a/app/code/Magento/Customer/view/frontend/web/template/authentication-popup.html b/app/code/Magento/Customer/view/frontend/web/template/authentication-popup.html index 9ad2cf5265043b4fd1850514258d99276d500ff3..db9c0a3819ffdbaf09b4cfb4ea1d4385909160d9 100644 --- a/app/code/Magento/Customer/view/frontend/web/template/authentication-popup.html +++ b/app/code/Magento/Customer/view/frontend/web/template/authentication-popup.html @@ -50,7 +50,7 @@ <div class="block-content" aria-labelledby="block-customer-login-heading"> <form class="form form-login" method="post" - data-bind="submit:login" + data-bind="event: {submit: login }" id="login-form"> <div class="fieldset login" data-bind="attr: {'data-hasrequired': $t('* Required Fields')}"> <div class="field email required"> diff --git a/app/code/Magento/Downloadable/etc/di.xml b/app/code/Magento/Downloadable/etc/di.xml index d7e2aafe79582d0f50a419cdc9bcf7f2f090cdcd..8e3d0bb6c5c5ae598a8365770f7e2ae932c78d73 100644 --- a/app/code/Magento/Downloadable/etc/di.xml +++ b/app/code/Magento/Downloadable/etc/di.xml @@ -123,4 +123,25 @@ </argument> </arguments> </type> + <type name="Magento\Catalog\Model\Product\Price\CostStorage"> + <arguments> + <argument name="allowedProductTypes" xsi:type="array"> + <item name="2" xsi:type="string">downloadable</item> + </argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Product\Price\TierPriceValidator"> + <arguments> + <argument name="allowedProductTypes" xsi:type="array"> + <item name="3" xsi:type="string">downloadable</item> + </argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Product\Price\BasePriceStorage"> + <arguments> + <argument name="allowedProductTypes" xsi:type="array"> + <item name="3" xsi:type="string">downloadable</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Paypal/Model/Express/Checkout.php b/app/code/Magento/Paypal/Model/Express/Checkout.php index 0f96b14124d042e86adbad3e944b2bdc97ee2e1a..893eda8cc6020882763056e78f844ab19166c1a6 100644 --- a/app/code/Magento/Paypal/Model/Express/Checkout.php +++ b/app/code/Magento/Paypal/Model/Express/Checkout.php @@ -7,7 +7,9 @@ namespace Magento\Paypal\Model\Express; use Magento\Customer\Api\Data\CustomerInterface as CustomerDataObject; use Magento\Customer\Model\AccountManagement; +use Magento\Framework\App\ObjectManager; use Magento\Paypal\Model\Config as PaypalConfig; +use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Order\Email\Sender\OrderSender; use Magento\Quote\Model\Quote\Address; use Magento\Framework\DataObject; @@ -268,6 +270,11 @@ class Checkout */ protected $totalsCollector; + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + /** * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Customer\Model\Url $customerUrl @@ -789,7 +796,8 @@ class Checkout $this->ignoreAddressValidation(); $this->_quote->collectTotals(); - $order = $this->quoteManagement->submit($this->_quote); + $orderId = $this->quoteManagement->placeOrder($this->_quote->getId()); + $order = $this->getOrderRepository()->get($orderId); if (!$order) { return; @@ -1157,4 +1165,20 @@ class Checkout ->setCustomerGroupId(\Magento\Customer\Model\Group::NOT_LOGGED_IN_ID); return $this; } + + /** + * Returns order repository instance + * + * @return OrderRepositoryInterface + * @deprecated + */ + private function getOrderRepository() + { + if ($this->orderRepository === null) { + $this->orderRepository = ObjectManager::getInstance() + ->get(OrderRepositoryInterface::class); + } + + return $this->orderRepository; + } } diff --git a/app/code/Magento/Search/Controller/Adminhtml/Term/ExportSearchCsv.php b/app/code/Magento/Search/Controller/Adminhtml/Term/ExportSearchCsv.php index 0d1de5cf80cc3463a8c24f3c36a61b4b40286d94..6b13fbc24953292a01326a819aeb8e325711eba2 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Term/ExportSearchCsv.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Term/ExportSearchCsv.php @@ -40,6 +40,6 @@ class ExportSearchCsv extends TermController /** @var \Magento\Framework\View\Result\Layout $resultLayout */ $resultLayout = $this->resultFactory->create(ResultFactory::TYPE_LAYOUT); $content = $resultLayout->getLayout()->getChildBlock('adminhtml.report.search.grid', 'grid.export'); - return $this->fileFactory->create(\search.csv::class, $content->getCsvFile(), DirectoryList::VAR_DIR); + return $this->fileFactory->create('search.csv', $content->getCsvFile(), DirectoryList::VAR_DIR); } } diff --git a/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Term/ExportSearchCsvTest.php b/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Term/ExportSearchCsvTest.php new file mode 100644 index 0000000000000000000000000000000000000000..041d14899d5b683693175399cf2836d7b56209b1 --- /dev/null +++ b/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Term/ExportSearchCsvTest.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Search\Test\Unit\Controller\Adminhtml\Term; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\App\Filesystem\DirectoryList; + +class ExportSearchCsvTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\TaxImportExport\Controller\Adminhtml\Rate\ExportPost + */ + private $controller; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $fileFactoryMock; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManagerHelper; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $resultFactoryMock; + + protected function setUp() + { + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->fileFactoryMock = $this->getMock( + \Magento\Framework\App\Response\Http\FileFactory::class, + [], + [], + '', + false + ); + $this->resultFactoryMock = $this->getMock( + \Magento\Framework\Controller\ResultFactory::class, + [], + [], + '', + false + ); + + $this->controller = $this->objectManagerHelper->getObject( + \Magento\Search\Controller\Adminhtml\Term\ExportSearchCsv::class, + [ + 'fileFactory' => $this->fileFactoryMock, + 'resultFactory' => $this->resultFactoryMock + ] + ); + } + + public function testExecute() + { + $resultLayoutMock = $this->getMock(\Magento\Framework\View\Result\Layout::class, [], [], '', false); + $layoutMock = $this->getMock(\Magento\Framework\View\LayoutInterface::class); + $contentMock = $this->getMock( + \Magento\Framework\View\Element\AbstractBlock::class, + ['getCsvFile'], + [], + '', + false + ); + $this->resultFactoryMock + ->expects($this->once()) + ->method('create') + ->with(ResultFactory::TYPE_LAYOUT)->willReturn($resultLayoutMock); + $resultLayoutMock->expects($this->once())->method('getLayout')->willReturn($layoutMock); + $layoutMock->expects($this->once())->method('getChildBlock')->willReturn($contentMock); + $contentMock->expects($this->once())->method('getCsvFile')->willReturn('csvFile'); + $this->fileFactoryMock + ->expects($this->once()) + ->method('create') + ->with('search.csv', 'csvFile', DirectoryList::VAR_DIR); + $this->controller->execute(); + } +} diff --git a/app/code/Magento/TaxImportExport/Controller/Adminhtml/Rate/ExportPost.php b/app/code/Magento/TaxImportExport/Controller/Adminhtml/Rate/ExportPost.php index cceb0c6de9fd63b4cf773ad778a27967bad8033d..351a2fe50d46a3b178be6514acbb9244ed2f53ad 100644 --- a/app/code/Magento/TaxImportExport/Controller/Adminhtml/Rate/ExportPost.php +++ b/app/code/Magento/TaxImportExport/Controller/Adminhtml/Rate/ExportPost.php @@ -81,7 +81,7 @@ class ExportPost extends \Magento\TaxImportExport\Controller\Adminhtml\Rate $content .= $rate->toString($template) . "\n"; } - return $this->fileFactory->create(\tax_rates.csv::class, $content, DirectoryList::VAR_DIR); + return $this->fileFactory->create('tax_rates.csv', $content, DirectoryList::VAR_DIR); } /** diff --git a/app/code/Magento/TaxImportExport/Test/Unit/Controller/Adminhtml/Rate/ExportPostTest.php b/app/code/Magento/TaxImportExport/Test/Unit/Controller/Adminhtml/Rate/ExportPostTest.php new file mode 100644 index 0000000000000000000000000000000000000000..744ad42c66d39d7358e7a28f2b98ec45fabadf11 --- /dev/null +++ b/app/code/Magento/TaxImportExport/Test/Unit/Controller/Adminhtml/Rate/ExportPostTest.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\TaxImportExport\Test\Unit\Controller\Adminhtml\Rate; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Framework\App\Filesystem\DirectoryList; + +class ExportPostTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\TaxImportExport\Controller\Adminhtml\Rate\ExportPost + */ + private $controller; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $fileFactoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $objectManagerMock; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManagerHelper; + + protected function setUp() + { + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->fileFactoryMock = $this->getMock( + \Magento\Framework\App\Response\Http\FileFactory::class, + [], + [], + '', + false + ); + $this->objectManagerMock = $this->getMock(\Magento\Framework\ObjectManagerInterface::class); + $this->controller = $this->objectManagerHelper->getObject( + \Magento\TaxImportExport\Controller\Adminhtml\Rate\ExportPost::class, + [ + 'fileFactory' => $this->fileFactoryMock, + 'objectManager' => $this->objectManagerMock + ] + ); + } + + public function testExecute() + { + $headers = new \Magento\Framework\DataObject( + [ + 'code' => __('Code'), + 'country_name' => __('Country'), + 'region_name' => __('State'), + 'tax_postcode' => __('Zip/Post Code'), + 'rate' => __('Rate'), + 'zip_is_range' => __('Zip/Post is Range'), + 'zip_from' => __('Range From'), + 'zip_to' => __('Range To'), + ] + ); + $template = '"{{code}}","{{country_name}}","{{region_name}}","{{tax_postcode}}","{{rate}}"' . + ',"{{zip_is_range}}","{{zip_from}}","{{zip_to}}"'; + $content = $headers->toString($template); + $content .= "\n"; + $storeMock = $this->getMock(\Magento\Store\Model\Store::class, [], [], '', false); + $storeCollectionMock = $this->objectManagerHelper->getCollectionMock( + \Magento\Store\Model\ResourceModel\Store\Collection::class, + [] + ); + $rateCollectionMock = $this->objectManagerHelper->getCollectionMock( + \Magento\Tax\Model\ResourceModel\Calculation\Rate\Collection::class, + [] + ); + + $taxCollectionMock = $this->objectManagerHelper->getCollectionMock( + \Magento\Tax\Model\ResourceModel\Calculation\Rate\Title\Collection::class, + [] + ); + $storeCollectionMock->expects($this->once())->method('setLoadDefault')->willReturnSelf(); + $rateTitleMock = $this->getMock(\Magento\Tax\Model\Calculation\Rate\Title::class, [], [], '', false); + $rateTitleMock->expects($this->once())->method('getCollection')->willReturn($taxCollectionMock); + $storeMock->expects($this->once())->method('getCollection')->willReturn($storeCollectionMock); + $this->objectManagerMock->expects($this->any())->method('create')->willReturnMap([ + [\Magento\Store\Model\Store::class, [], $storeMock], + [\Magento\Tax\Model\Calculation\Rate\Title::class, [], $rateTitleMock], + [\Magento\Tax\Model\ResourceModel\Calculation\Rate\Collection::class, [], $rateCollectionMock] + ]); + $rateCollectionMock->expects($this->once())->method('joinCountryTable')->willReturnSelf(); + $rateCollectionMock->expects($this->once())->method('joinRegionTable')->willReturnSelf(); + $this->fileFactoryMock + ->expects($this->once()) + ->method('create') + ->with('tax_rates.csv', $content, DirectoryList::VAR_DIR); + $this->controller->execute(); + } +} 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 449418f07c100c91e139231c704bb28fd8c9ae82..bfae1cf87030f45d8bbda74e8eb3d9f741f00201 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 @@ -538,7 +538,8 @@ define([ label: cell.config.label, name: cell.name, required: !!cell.config.validation, - columnsHeaderClasses: cell.config.columnsHeaderClasses + columnsHeaderClasses: cell.config.columnsHeaderClasses, + sortOrder: cell.config.sortOrder }); this.labels.push(data); diff --git a/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less index 799094cf93d7cfa4ffcfb6b32a2767e93c882a06..3273df52b8d622146ab134184ec607503938e51a 100644 --- a/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less @@ -319,6 +319,11 @@ .order-products-toolbar { position: relative; + + .toolbar-amount { + position: relative; + text-align: center; + } } } diff --git a/app/design/frontend/Magento/luma/Magento_Review/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Review/web/css/source/_module.less index 95ec8b694b1ca5a24dc9af66120acbc857294e5b..c8c885fafda19d5ca6592ff22f37ce0612f82694 100644 --- a/app/design/frontend/Magento/luma/Magento_Review/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Review/web/css/source/_module.less @@ -40,6 +40,7 @@ .review-control-vote { .lib-rating-vote(); + &:before { .lib-rating-icons-content( @_icon-content: @icon-star-empty @@ -47,6 +48,19 @@ } } + // + // Account Review list page + // ----------------------------------------- + + .products-reviews-toolbar { + position: relative; + + .toolbar-amount { + position: relative; + text-align: center; + } + } + // // Review product page // ----------------------------------------- @@ -95,6 +109,7 @@ .fieldset &-legend.legend { border-bottom: 0; line-height: 1.3; + margin-bottom: @indent__base; padding: 0; span { @@ -105,8 +120,6 @@ display: block; font-weight: 600; } - - margin-bottom: @indent__base; } .fieldset &-field-ratings { @@ -189,6 +202,7 @@ margin-bottom: @indent__base; } } + .page-main { .column { .review-add { @@ -284,9 +298,6 @@ a:not(:last-child) { margin-right: 30px; } - - .action.view span { - } } } @@ -331,6 +342,7 @@ .items { .item { .lib-css(margin-bottom, @indent__base); + &:last-child { margin-bottom: 0; } @@ -339,6 +351,7 @@ .product-name { display: inline-block; + &:not(:last-child) { .lib-css(margin-bottom, @indent__xs); } @@ -415,9 +428,6 @@ width: 30%; } - .product-info { - } - .review-details { margin: 0; diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/BasePriceStorageTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/BasePriceStorageTest.php new file mode 100644 index 0000000000000000000000000000000000000000..aad068ce6f0799feb342e93b3f94c6e6899c18c9 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/BasePriceStorageTest.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Api; + +use Magento\TestFramework\TestCase\WebapiAbstract; + +/** + * BasePriceStorage test. + */ +class BasePriceStorageTest extends WebapiAbstract +{ + const SERVICE_NAME = 'catalogBasePriceStorageV1'; + const SERVICE_VERSION = 'V1'; + const SIMPLE_PRODUCT_SKU = 'simple'; + + /** + * @var \Magento\TestFramework\ObjectManager + */ + private $objectManager; + + /** + * Set up. + */ + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * Test get method. + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testGet() + { + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/products/base-prices-information', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Get', + ], + ]; + $response = $this->_webApiCall($serviceInfo, ['skus' => [self::SIMPLE_PRODUCT_SKU]]); + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $product = $productRepository->get(self::SIMPLE_PRODUCT_SKU); + + $this->assertNotEmpty($response); + $this->assertEquals($product->getPrice(), $response[0]['price']); + $this->assertEquals($product->getSku(), $response[0]['sku']); + } + + /** + * Test update method. + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testUpdate() + { + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/products/base-prices', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Update', + ], + ]; + $newPrice = 9999; + $storeId = 0; + $response = $this->_webApiCall( + $serviceInfo, + [ + 'prices' => [ + [ + 'price' => $newPrice, + 'store_id' => $storeId, + 'sku' => self::SIMPLE_PRODUCT_SKU, + ] + ] + ] + ); + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $product = $productRepository->get(self::SIMPLE_PRODUCT_SKU); + + $this->assertNotEmpty($response); + $this->assertEquals($product->getPrice(), $newPrice); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CostStorageTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CostStorageTest.php new file mode 100644 index 0000000000000000000000000000000000000000..6af9960a524dfe63910b51dbee08335af109d909 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CostStorageTest.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Api; + +use Magento\TestFramework\TestCase\WebapiAbstract; + +/** + * CostStorage test. + */ +class CostStorageTest extends WebapiAbstract +{ + const SERVICE_NAME = 'catalogCostStorageV1'; + const SERVICE_VERSION = 'V1'; + const SIMPLE_PRODUCT_SKU = 'simple'; + + /** + * @var \Magento\TestFramework\ObjectManager + */ + private $objectManager; + + /** + * Set up. + */ + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * Test get method. + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testGet() + { + $cost = 3057; + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $productRepository->save($productRepository->get(self::SIMPLE_PRODUCT_SKU)->setData('cost', $cost)); + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/products/cost-information', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Delete', + ], + ]; + $response = $this->_webApiCall($serviceInfo, ['skus' => [self::SIMPLE_PRODUCT_SKU]]); + + /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $product = $productRepository->get(self::SIMPLE_PRODUCT_SKU); + + $this->assertNotEmpty($response); + $this->assertEquals($product->getCost(), $cost); + } + + /** + * Test update method. + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testUpdate() + { + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/products/cost', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Update', + ], + ]; + $storeId = 0; + $newCost = 31337; + $response = $this->_webApiCall( + $serviceInfo, + [ + 'prices' => [ + [ + 'cost' => $newCost, + 'store_id' => $storeId, + 'sku' => self::SIMPLE_PRODUCT_SKU, + ] + ] + ] + ); + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $product = $productRepository->get(self::SIMPLE_PRODUCT_SKU); + $this->assertNotEmpty($response); + $this->assertEquals($product->getCost(), $newCost); + } + + /** + * Test delete method. + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testDelete() + { + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $productRepository->save($productRepository->get(self::SIMPLE_PRODUCT_SKU)->setData('cost', 777)); + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/products/cost-delete', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Delete', + ], + ]; + $response = $this->_webApiCall($serviceInfo, ['skus' => [self::SIMPLE_PRODUCT_SKU]]); + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $product = $productRepository->get(self::SIMPLE_PRODUCT_SKU); + $this->assertTrue($response); + $this->assertNull($product->getCost()); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/TierPriceStorageTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/TierPriceStorageTest.php new file mode 100644 index 0000000000000000000000000000000000000000..232c63257215036e414236fba818ff876da97c18 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/TierPriceStorageTest.php @@ -0,0 +1,231 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Api; + +use Magento\TestFramework\TestCase\WebapiAbstract; + +/** + * TierPriceStorage test. + */ +class TierPriceStorageTest extends WebapiAbstract +{ + const SERVICE_NAME = 'catalogTierPriceStorageV1'; + const SERVICE_VERSION = 'V1'; + const SIMPLE_PRODUCT_SKU = 'simple'; + + /** + * @var \Magento\TestFramework\ObjectManager + */ + private $objectManager; + + /** + * Set up. + */ + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * Test get method. + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testGet() + { + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/products/tier-prices-information', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Get', + ], + ]; + $response = $this->_webApiCall($serviceInfo, ['skus' => [self::SIMPLE_PRODUCT_SKU]]); + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $tierPrices = $productRepository->get(self::SIMPLE_PRODUCT_SKU)->getTierPrices(); + $this->assertNotEmpty($response); + $this->assertEquals(count($response), count($tierPrices)); + + foreach ($response as $item) { + $this->assertTrue($this->isPriceCorrect($item, $tierPrices)); + } + } + + /** + * Test update method. + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testUpdate() + { + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $prices = $productRepository->get(self::SIMPLE_PRODUCT_SKU)->getTierPrices(); + $tierPrice = array_shift($prices); + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/products/tier-prices', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Update', + ], + ]; + $newPrice = [ + 'price' => 40, + 'price_type' => \Magento\Catalog\Api\Data\TierPriceInterface::PRICE_TYPE_DISCOUNT, + 'website_id' => 0, + 'sku' => self::SIMPLE_PRODUCT_SKU, + 'customer_group' => 'ALL GROUPS', + 'quantity' => 7778 + ]; + $updatedPrice = [ + 'price' => 778, + 'price_type' => \Magento\Catalog\Api\Data\TierPriceInterface::PRICE_TYPE_FIXED, + 'website_id' => 0, + 'sku' => self::SIMPLE_PRODUCT_SKU, + 'customer_group' => 'ALL GROUPS', + 'quantity' => $tierPrice->getQty() + ]; + $response = $this->_webApiCall($serviceInfo, ['prices' => [$updatedPrice, $newPrice]]); + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $tierPrices = $productRepository->get(self::SIMPLE_PRODUCT_SKU)->getTierPrices(); + $this->assertTrue($response); + $this->assertTrue($this->isPriceCorrect($newPrice, $tierPrices)); + $this->assertTrue($this->isPriceCorrect($updatedPrice, $tierPrices)); + } + + /** + * Test replace method. + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testReplace() + { + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/products/tier-prices', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Replace', + ], + ]; + $newPrices = [ + [ + 'price' => 50, + 'price_type' => \Magento\Catalog\Api\Data\TierPriceInterface::PRICE_TYPE_DISCOUNT, + 'website_id' => 0, + 'sku' => self::SIMPLE_PRODUCT_SKU, + 'customer_group' => 'general', + 'quantity' => 7778 + ], + [ + 'price' => 70, + 'price_type' => \Magento\Catalog\Api\Data\TierPriceInterface::PRICE_TYPE_FIXED, + 'website_id' => 0, + 'sku' => self::SIMPLE_PRODUCT_SKU, + 'customer_group' => 'general', + 'quantity' => 33 + ] + ]; + $response = $this->_webApiCall($serviceInfo, ['prices' => $newPrices]); + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $tierPrices = $productRepository->get(self::SIMPLE_PRODUCT_SKU)->getTierPrices(); + $this->assertTrue($response); + $this->assertEquals(count($newPrices), count($tierPrices)); + + foreach ($newPrices as $newPrice) { + $this->assertTrue($this->isPriceCorrect($newPrice, $tierPrices)); + } + } + + /** + * Test delete method. + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testDelete() + { + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $tierPrices = $productRepository->get(self::SIMPLE_PRODUCT_SKU)->getTierPrices(); + $pricesToStore = array_pop($tierPrices); + $pricesToDelete = []; + foreach ($tierPrices as $tierPrice) { + $tierPriceValue = $tierPrice->getExtensionAttributes()->getPercentageValue() + ?: $tierPrice->getValue(); + $priceType = $tierPrice->getExtensionAttributes()->getPercentageValue() + ? \Magento\Catalog\Api\Data\TierPriceInterface::PRICE_TYPE_DISCOUNT + : \Magento\Catalog\Api\Data\TierPriceInterface::PRICE_TYPE_FIXED; + $customerGroup = $tierPrice->getCustomerGroupId() == \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID + ? 'NOT LOGGED IN' + : 'ALL GROUPS'; + $pricesToDelete[] = [ + 'price' => $tierPriceValue, + 'price_type' => $priceType, + 'website_id' => 0, + 'customer_group' => $customerGroup, + 'sku' => self::SIMPLE_PRODUCT_SKU, + 'quantity' => $tierPrice->getQty() + + ]; + } + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/products/tier-prices-delete', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Delete', + ], + ]; + $response = $this->_webApiCall($serviceInfo, ['prices' => $pricesToDelete]); + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $tierPrices = $productRepository->get(self::SIMPLE_PRODUCT_SKU)->getTierPrices(); + $tierPrice = $tierPrices[0]; + $this->assertTrue($response); + $this->assertEquals(1, count($tierPrices)); + $this->assertEquals($pricesToStore, $tierPrice); + } + + /** + * Check prise exists and is correct. + * + * @param array $price + * @param array $tierPrices + * @return bool + */ + private function isPriceCorrect(array $price, array $tierPrices) + { + $isCorrect = false; + + foreach ($tierPrices as $tierPrice) { + $priceIsCorrect = $price['price_type'] === \Magento\Catalog\Api\Data\TierPriceInterface::PRICE_TYPE_DISCOUNT + ? (float)$tierPrice->getExtensionAttributes()->getPercentageValue() === (float)$price['price'] + : (float)$tierPrice->getValue() === (float)$price['price']; + if ( + $priceIsCorrect + && (int)$tierPrice->getQty() === (int)$price['quantity'] + && $tierPrice->getExtensionAttributes()->getWebsiteId() == $price['website_id'] + ) { + $isCorrect = true; + } + } + + return $isCorrect; + } +} diff --git a/dev/tests/functional/.htaccess.sample b/dev/tests/functional/.htaccess.sample index f5c3be7db01e97c551085e09a493fceb11e2ad1f..f1e60cf9b67e91779153e4c9d9f780c8f7bc9438 100644 --- a/dev/tests/functional/.htaccess.sample +++ b/dev/tests/functional/.htaccess.sample @@ -1,6 +1,6 @@ ############################################## ## Allow access to command.php and website.php - <FilesMatch "command.php|website.php"> + <FilesMatch "command.php|website.php|export.php"> order allow,deny allow from all </FilesMatch> diff --git a/dev/tests/functional/composer.json b/dev/tests/functional/composer.json index f170114a4ea8348714531af55ca805bb5e3442fe..3567ec530f437996c0ed28d792ebd929d2188e0c 100644 --- a/dev/tests/functional/composer.json +++ b/dev/tests/functional/composer.json @@ -1,6 +1,6 @@ { "require": { - "magento/mtf": "1.0.0-rc50", + "magento/mtf": "1.0.0-rc51", "php": "~5.6.5|7.0.2|~7.0.6", "phpunit/phpunit": "~4.8.0|~5.5.0", "phpunit/phpunit-selenium": ">=1.2" diff --git a/dev/tests/functional/etc/di.xml b/dev/tests/functional/etc/di.xml index 69951a2cc1b86248040074c45ee967c93dc0bb82..b21e507a5af023b18326d2c1cf5bc038a5abdb3a 100644 --- a/dev/tests/functional/etc/di.xml +++ b/dev/tests/functional/etc/di.xml @@ -6,6 +6,11 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\Mtf\Util\Command\File\ExportInterface" type="\Magento\Mtf\Util\Command\File\Export" /> + <preference for="Magento\Mtf\Util\Command\File\Export\ReaderInterface" type="\Magento\Mtf\Util\Command\File\Export\Reader" /> + + <type name="\Magento\Mtf\Util\Command\File\Export" shared="false" /> + <virtualType name="Magento\Mtf\Config\SchemaLocator\Config" type="Magento\Mtf\Config\SchemaLocator"> <arguments> <argument name="schemaPath" xsi:type="string">etc/config.xsd</argument> @@ -13,4 +18,22 @@ </virtualType> <type name="Magento\Mtf\Util\Protocol\CurlTransport\WebapiDecorator" shared="true" /> + + <type name="Magento\Mtf\Util\Command\File\Export\Reader"> + <arguments> + <argument name="template" xsi:type="string">\w*?\.csv</argument> + </arguments> + </type> + + <virtualType name="Magento\Mtf\Util\Command\File\Export\ProductReader" type="Magento\Mtf\Util\Command\File\Export\Reader"> + <arguments> + <argument name="template" xsi:type="string">catalog_product.*?\.csv</argument> + </arguments> + </virtualType> + + <virtualType name="Magento\Mtf\Util\Command\File\Export\CustomerReader" type="Magento\Mtf\Util\Command\File\Export\Reader"> + <arguments> + <argument name="template" xsi:type="string">customer.*?\.csv</argument> + </arguments> + </virtualType> </config> diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli.php index 96be468a6eb377f619339c02a2be00835f8425eb..db39bdcb5b7a5e1e323ee434b2e8b1d744b0570a 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli.php +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli.php @@ -7,7 +7,6 @@ namespace Magento\Mtf\Util\Command; use Magento\Mtf\Util\Protocol\CurlInterface; -use Magento\Mtf\ObjectManager; use Magento\Mtf\Util\Protocol\CurlTransport; /** @@ -28,7 +27,6 @@ class Cli private $transport; /** - * @constructor * @param CurlTransport $transport */ public function __construct(CurlTransport $transport) @@ -61,6 +59,6 @@ class Cli private function prepareUrl($command, array $options) { $command .= ' ' . implode(' ', $options); - return $_ENV['app_frontend_url'] . Cli::URL . '?command=' . urlencode($command); + return $_ENV['app_frontend_url'] . self::URL . '?command=' . urlencode($command); } } diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export.php new file mode 100644 index 0000000000000000000000000000000000000000..6d5defadbc44a5d30e69423cafe96653450f51bf --- /dev/null +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export.php @@ -0,0 +1,131 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Mtf\Util\Command\File; + +use Magento\Mtf\ObjectManagerInterface; +use Magento\Mtf\Util\Command\File\Export\Data; +use Magento\Mtf\Util\Command\File\Export\ReaderInterface; + +/** + * Get Exporting file from the Magento. + */ +class Export implements ExportInterface +{ + /** + * Path to the Reader. + * + * @var string + */ + private $readerPath = 'Magento\Mtf\Util\Command\File\Export\%sReader'; + + /** + * Object manager instance. + * + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * File reader for Magento export files. + * + * @var ReaderInterface + */ + private $reader; + + /** + * @param ObjectManagerInterface $objectManager + * @param string $type [optional] + */ + public function __construct(ObjectManagerInterface $objectManager, $type = 'product') + { + $this->objectManager = $objectManager; + $this->reader = $this->getReader($type); + } + + /** + * Get reader for export files. + * + * @param string $type + * @return ReaderInterface + * @throws \ReflectionException + */ + private function getReader($type) + { + $readerPath = sprintf($this->readerPath, ucfirst($type)); + try { + return $this->objectManager->create($readerPath); + } catch (\ReflectionException $e) { + throw new \ReflectionException("Virtual type '$readerPath' does not exist. Please, check it in di.xml."); + } + } + + /** + * Get the export file by name. + * + * @param string $name + * @return Data|null + */ + public function getByName($name) + { + $this->reader->getData(); + foreach ($this->reader->getData() as $file) { + if ($file->getName() === $name) { + return $file; + } + } + + return null; + } + + /** + * Get latest created the export file. + * + * @return Data|null + */ + public function getLatest() + { + $max = 0; + $latest = null; + foreach ($this->reader->getData() as $file) { + if ($file->getDate() > $max) { + $max = $file->getDate(); + $latest = $file; + } + } + + return $latest; + } + + /** + * Get all export files by date range using unix time stamp. + * + * @param string $start + * @param string $end + * @return Data[] + */ + public function getByDateRange($start, $end) + { + $files = []; + foreach ($this->reader->getData() as $file) { + if ($file->getDate() > $start && $file->getDate() < $end) { + $files[] = $file; + } + } + + return $files; + } + + /** + * Get all export files. + * + * @return Data[] + */ + public function getAll() + { + return $this->reader->getData(); + } +} diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export/Data.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export/Data.php new file mode 100644 index 0000000000000000000000000000000000000000..a6756aac0052a28bc04cc7c7d420c4ace8d5c472 --- /dev/null +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export/Data.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Mtf\Util\Command\File\Export; + +/** + * Data mapping for Export file. + */ +class Data +{ + /** + * File data. + * + * @var array + */ + private $data; + + /** + * @param array $data + */ + public function __construct(array $data) + { + $this->data = $data; + } + + /** + * Get file name. + * + * @return string + */ + public function getName() + { + return $this->data['name']; + } + + /** + * Get file content. + * + * @return string + */ + public function getContent() + { + return $this->data['content']; + } + + /** + * Get file creation date. + * + * @return string + */ + public function getDate() + { + return $this->data['date']; + } +} diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export/Reader.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export/Reader.php new file mode 100644 index 0000000000000000000000000000000000000000..3a8287daae2ab54cc8adbd8fff2249864cbed9a5 --- /dev/null +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export/Reader.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Mtf\Util\Command\File\Export; + +use Magento\Mtf\ObjectManagerInterface; +use Magento\Mtf\Util\Protocol\CurlTransport; +use Magento\Mtf\Util\Protocol\CurlInterface; + +/** + * File reader for Magento export files. + */ +class Reader implements ReaderInterface +{ + /** + * Pattern for file name in Magento. + * + * @var string + */ + private $template; + + /** + * Object manager instance. + * + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * Curl transport protocol. + * + * @var CurlTransport + */ + private $transport; + + /** + * @param ObjectManagerInterface $objectManager + * @param CurlTransport $transport + * @param string $template + */ + public function __construct(ObjectManagerInterface $objectManager, CurlTransport $transport, $template) + { + $this->objectManager = $objectManager; + $this->template = $template; + $this->transport = $transport; + } + + /** + * Exporting files as Data object from Magento. + * + * @return Data[] + */ + public function getData() + { + $data = []; + foreach ($this->getFiles() as $file) { + $data[] = $this->objectManager->create(Data::class, ['data' => $file]); + } + + return $data; + } + + /** + * Get files by template from the Magento. + * + * @return array + */ + private function getFiles() + { + $this->transport->write($this->prepareUrl(), [], CurlInterface::GET); + $serializedFiles = $this->transport->read(); + $this->transport->close(); + + return unserialize($serializedFiles); + } + + /** + * Prepare url. + * + * @return string + */ + private function prepareUrl() + { + return $_ENV['app_frontend_url'] . self::URL . '?template=' . urlencode($this->template); + } +} diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export/ReaderInterface.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export/ReaderInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..5d305513ec68080ec4e7ec6bad8e776f8653bf15 --- /dev/null +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export/ReaderInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Mtf\Util\Command\File\Export; + +/** + * File reader interface for Magento export files. + */ +interface ReaderInterface +{ + /** + * Url to export.php. + */ + const URL = 'dev/tests/functional/utils/export.php'; + + /** + * Exporting files as Data object from Magento. + * + * @return Data[] + */ + public function getData(); +} diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/ExportInterface.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/ExportInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..0bc4f9e59a2433b1078805e8431f9f45d8d34e20 --- /dev/null +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/ExportInterface.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Mtf\Util\Command\File; + +use Magento\Mtf\Util\Command\File\Export\Data; + +/** + * Interface for getting Exporting file from the Magento. + */ +interface ExportInterface +{ + /** + * Get the export file by name. + * + * @param string $name + * @return Data|null + */ + public function getByName($name); + + /** + * Get latest created the export file. + * + * @return Data|null + */ + public function getLatest(); + + /** + * Get all export files by date range using unix time stamp. + * + * @param string $start + * @param string $end + * @return Data[] + */ + public function getByDateRange($start, $end); + + /** + * Get all export files. + * + * @return Data[] + */ + public function getAll(); +} diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/Website.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Website.php index 611f46894c8bca850d10a519fe7a2850f31b5533..d0c4be34888d2ae863173fbbedc203c08d95fb90 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Util/Command/Website.php +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Website.php @@ -58,6 +58,6 @@ class Website */ private function prepareUrl($websiteCode) { - return $_ENV['app_frontend_url'] . Website::URL . '?website_code=' . urlencode($websiteCode); + return $_ENV['app_frontend_url'] . self::URL . '?website_code=' . urlencode($websiteCode); } } diff --git a/dev/tests/functional/tests/app/Magento/Braintree/Test/TestCase/InvoicePayPalBraintreeTest.php b/dev/tests/functional/tests/app/Magento/Braintree/Test/TestCase/InvoicePayPalBraintreeTest.php index 062de338d8921e15b786de0fd4b656e824354d80..ca5fbe8799d5144a2a9e68e7b8fbca4530c26790 100644 --- a/dev/tests/functional/tests/app/Magento/Braintree/Test/TestCase/InvoicePayPalBraintreeTest.php +++ b/dev/tests/functional/tests/app/Magento/Braintree/Test/TestCase/InvoicePayPalBraintreeTest.php @@ -33,7 +33,7 @@ class InvoicePayPalBraintreeTest extends Scenario /* end tags */ /** - * Runs one page checkout test. + * Create invoice for order placed within Braintree PayPal. * * @return void */ diff --git a/dev/tests/functional/tests/app/Magento/Braintree/Test/TestCase/InvoicePaypalBraintreeTest.xml b/dev/tests/functional/tests/app/Magento/Braintree/Test/TestCase/InvoicePaypalBraintreeTest.xml index 23985d208e5a675b997d330e792eb58f1045d7d9..1d98e2d792a3e75fd815fe78149e43100aad12ae 100644 --- a/dev/tests/functional/tests/app/Magento/Braintree/Test/TestCase/InvoicePaypalBraintreeTest.xml +++ b/dev/tests/functional/tests/app/Magento/Braintree/Test/TestCase/InvoicePaypalBraintreeTest.xml @@ -31,7 +31,7 @@ <constraint name="Magento\Sales\Test\Constraint\AssertOrderButtonsAvailable" /> <constraint name="Magento\Sales\Test\Constraint\AssertInvoiceItems" /> </variation> - <variation name="InvoicePayPalBraintreeTestVariation2" summary="Partial capture for order placed within Braintree PayPal" ticketId="MAGETWO-48615"> + <variation name="InvoicePayPalBraintreeTestVariation2" summary="Partial capture for order placed within Braintree PayPal" ticketId="MAGETWO-48615, MAGETWO-48684"> <data name="description" xsi:type="string">Partial capture for order placed within Braintree PayPal</data> <data name="products/0" xsi:type="string">catalogProductSimple::product_100_dollar</data> <data name="taxRule" xsi:type="string">us_illinois_tax_rule</data> @@ -43,7 +43,6 @@ </data> <data name="capturedPrices" xsi:type="array"> <item name="0" xsi:type="string">118.25</item> - <item name="1" xsi:type="string">108.25</item> </data> <data name="payment/method" xsi:type="string">braintree_paypal</data> <data name="configData" xsi:type="string">braintree, braintree_paypal, braintree_paypal_skip_order_review</data> @@ -51,10 +50,18 @@ <data name="data/items_data/0/qty" xsi:type="string">1</data> <data name="data/form_data/do_shipment" xsi:type="string">No</data> <data name="data/form_data/comment_text" xsi:type="string">comments</data> - <data name="tag" xsi:type="string">test_type:3rd_party_test, severity:S1</data> + <data name="status" xsi:type="string">Processing</data> + <data name="transactions/Capture" xsi:type="array"> + <item name="transactionType" xsi:type="string">Capture</item> + <item name="statusIsClosed" xsi:type="string">No</item> + </data> + <data name="tag" xsi:type="string">test_type:3rd_party_test, severity:S0</data> <constraint name="Magento\Sales\Test\Constraint\AssertInvoiceSuccessCreateMessage" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderButtonsAvailable" /> - <constraint name="Magento\Sales\Test\Constraint\AssertInvoiceItems" /> + <constraint name="Magento\Sales\Test\Constraint\AssertCaptureInCommentsHistory" /> + <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusIsCorrect" /> + <constraint name="Magento\Sales\Test\Constraint\AssertInvoiceInInvoicesGrid" /> + <constraint name="Magento\Sales\Test\Constraint\AssertTransactionStatus" /> </variation> </testCase> </config> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertCancelSuccessMessageInShoppingCart.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertCancelSuccessMessageInShoppingCart.php new file mode 100644 index 0000000000000000000000000000000000000000..d549e14c7afd94bc35d2540662012fd9f970b872 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertCancelSuccessMessageInShoppingCart.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Checkout\Test\Constraint; + +use Magento\Checkout\Test\Page\CheckoutCart; +use Magento\Mtf\Constraint\AbstractConstraint; + +/** + * Assert that success message about canceled order is present and correct. + */ +class AssertCancelSuccessMessageInShoppingCart extends AbstractConstraint +{ + /** + * Cancel success message text. + */ + const SUCCESS_MESSAGE = 'Payment was canceled.'; + + /** + * Assert that success message about canceled order is present and correct. + * + * @param CheckoutCart $checkoutCart + * @return void + */ + public function processAssert(CheckoutCart $checkoutCart) + { + $actualMessage = $checkoutCart->getMessagesBlock()->getSuccessMessage(); + \PHPUnit_Framework_Assert::assertEquals( + self::SUCCESS_MESSAGE, + $actualMessage, + 'Success message is not present or has wrong text.' + ); + } + + /** + * Returns a string representation of the object. + * + * @return string + */ + public function toString() + { + return 'Cancel success message is present or has a correct text.'; + } +} diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/etc/di.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/etc/di.xml index 9f13d8dddb9a768442f7051a6e11be16f7df9233..0a0622936a23d7688cf0e94b61ebc2ef56d052f4 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/etc/di.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/etc/di.xml @@ -102,11 +102,6 @@ <argument name="severity" xsi:type="string">S2</argument> </arguments> </type> - <type name="Magento\Checkout\Test\Constraint\AssertProductPresentInShoppingCart"> - <arguments> - <argument name="severity" xsi:type="string">S2</argument> - </arguments> - </type> <type name="Magento\Checkout\Test\Constraint\AssertProductQtyInShoppingCart"> <arguments> <argument name="severity" xsi:type="string">S2</argument> @@ -157,4 +152,14 @@ <argument name="severity" xsi:type="string">S2</argument> </arguments> </type> + <type name="Magento\Checkout\Test\Constraint\AssertCancelSuccessMessageInShoppingCart"> + <arguments> + <argument name="severity" xsi:type="string">S1</argument> + </arguments> + </type> + <type name="Magento\Checkout\Test\Constraint\AssertProductPresentInShoppingCart"> + <arguments> + <argument name="severity" xsi:type="string">S0</argument> + </arguments> + </type> </config> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Repository/Address.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/Repository/Address.xml index 2edc328b5c5df40df321e66a34fa1ee7c6d4e835..6e7e76524557dd6ec875af966ef23e85bef6a73c 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/Repository/Address.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/Repository/Address.xml @@ -297,5 +297,17 @@ <field name="region_id" xsi:type="string">California</field> <field name="postcode" xsi:type="string">90230</field> </dataset> + + <dataset name="AVS_street_does_not_match_address"> + <field name="firstname" xsi:type="string">John</field> + <field name="lastname" xsi:type="string">Doe</field> + <field name="company" xsi:type="string">Magento %isolation%</field> + <field name="city" xsi:type="string">Culver City</field> + <field name="street" xsi:type="string">49354 Main</field> + <field name="telephone" xsi:type="string">555-55-555-55</field> + <field name="country_id" xsi:type="string">United States</field> + <field name="region_id" xsi:type="string">California</field> + <field name="postcode" xsi:type="string">90230</field> + </dataset> </repository> </config> diff --git a/dev/tests/functional/tests/app/Magento/Paypal/Test/Repository/ConfigData.xml b/dev/tests/functional/tests/app/Magento/Paypal/Test/Repository/ConfigData.xml index 1669352dc026ef02c295fce1153f63e3beabf225..f1d041f0a5b344fb76e91acd57124eb03d9f6ae8 100644 --- a/dev/tests/functional/tests/app/Magento/Paypal/Test/Repository/ConfigData.xml +++ b/dev/tests/functional/tests/app/Magento/Paypal/Test/Repository/ConfigData.xml @@ -25,54 +25,54 @@ </dataset> <dataset name="paypal_direct"> - <field name="payment/paypal_group_all_in_one/wpp_usuk/wpp_required_settings/wpp_and_express_checkout/business_account" xsi:type="array"> + <field name="payment/paypal_group_all_in_one/wpp_usuk/paypal_payflow_required/paypal_payflow_api_settings/business_account" xsi:type="array"> <item name="scope" xsi:type="string">payment</item> <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string">Yes</item> - <item name="value" xsi:type="string">PAYPAL_BUSINESS_ACCOUNT</item> + <item name="label" xsi:type="string">Email Associated with PayPal Merchant Account (Optional)</item> + <item name="value" xsi:type="string">%payflow_pro_business_account%</item> </field> - <field name="payment/paypal_group_all_in_one/wpp_usuk/wpp_required_settings/wpp_and_express_checkout/api_username" xsi:type="array"> + <field name="payment/paypal_group_all_in_one/wpp_usuk/paypal_payflow_required/paypal_payflow_api_settings/partner" xsi:type="array"> <item name="scope" xsi:type="string">payment</item> <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string"/> - <item name="value" xsi:type="string">PAYPAL_API_USERNAME</item> + <item name="label" xsi:type="string">Partner</item> + <item name="value" xsi:type="string">%payflow_pro_partner%</item> </field> - <field name="payment/paypal_group_all_in_one/wpp_usuk/wpp_required_settings/wpp_and_express_checkout/api_password" xsi:type="array"> + <field name="payment/paypal_group_all_in_one/wpp_usuk/paypal_payflow_required/paypal_payflow_api_settings/user" xsi:type="array"> <item name="scope" xsi:type="string">payment</item> <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string"/> - <item name="value" xsi:type="string">PAYPAL_API_PASSWORD</item> + <item name="label" xsi:type="string">User</item> + <item name="value" xsi:type="string">%payflow_pro_user%</item> </field> - <field name="payment/paypal_group_all_in_one/wpp_usuk/wpp_required_settings/wpp_and_express_checkout/api_signature" xsi:type="array"> + <field name="payment/paypal_group_all_in_one/wpp_usuk/paypal_payflow_required/paypal_payflow_api_settings/vendor" xsi:type="array"> <item name="scope" xsi:type="string">payment</item> <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string"/> - <item name="value" xsi:type="string">PAYPAL_API_SIGNATURE</item> + <item name="label" xsi:type="string">Vendor</item> + <item name="value" xsi:type="string">%payflow_pro_vendor%</item> </field> - <field name="payment/paypal_group_all_in_one/wpp_usuk/wpp_required_settings/wpp_and_express_checkout/sandbox_flag" xsi:type="array"> + <field name="payment/paypal_group_all_in_one/wpp_usuk/paypal_payflow_required/paypal_payflow_api_settings/pwd" xsi:type="array"> <item name="scope" xsi:type="string">payment</item> <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string">Yes</item> - <item name="value" xsi:type="number">1</item> + <item name="label" xsi:type="string">Password</item> + <item name="value" xsi:type="string">%payflow_pro_pwd%</item> </field> - <field name="payment/paypal_direct/debug" xsi:type="array"> + <field name="payment/paypal_group_all_in_one/wpp_usuk/paypal_payflow_required/paypal_payflow_api_settings/sandbox_flag" xsi:type="array"> <item name="scope" xsi:type="string">payment</item> <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string">Yes</item> + <item name="label" xsi:type="string">Test Mode</item> <item name="value" xsi:type="number">1</item> </field> - <field name="payment/paypal_direct/active" xsi:type="array"> + <field name="payment/paypal_group_all_in_one/wpp_usuk/paypal_payflow_required/enable_paypal_payflow" xsi:type="array"> <item name="scope" xsi:type="string">payment</item> <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string">Yes</item> + <item name="label" xsi:type="string">Enable this Solution</item> <item name="value" xsi:type="number">1</item> </field> </dataset> <dataset name="paypal_direct_rollback"> - <field name="payment/paypal_direct/active" xsi:type="array"> + <field name="payment/paypal_group_all_in_one/wpp_usuk/paypal_payflow_required/enable_paypal_payflow" xsi:type="array"> <item name="scope" xsi:type="string">payment</item> <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string">Yes</item> + <item name="label" xsi:type="string">Enable this Solution</item> <item name="value" xsi:type="number">0</item> </field> </dataset> @@ -219,6 +219,23 @@ </field> </dataset> + <dataset name="payflowpro_avs_street_does_not_match"> + <field name="payment/payflowpro/avs_street" xsi:type="array"> + <item name="scope" xsi:type="string">payment</item> + <item name="scope_id" xsi:type="number">1</item> + <item name="label" xsi:type="string">Yes</item> + <item name="value" xsi:type="number">1</item> + </field> + </dataset> + <dataset name="payflowpro_avs_street_does_not_match_rollback"> + <field name="payment/payflowpro/avs_street" xsi:type="array"> + <item name="scope" xsi:type="string">payment</item> + <item name="scope_id" xsi:type="number">1</item> + <item name="label" xsi:type="string">No</item> + <item name="value" xsi:type="number">0</item> + </field> + </dataset> + <dataset name="hosted_pro"> <field name="payment/paypal_group_all_in_one/payments_pro_hosted_solution_with_express_checkout/pphs_required_settings/pphs_required_settings_pphs/business_account" xsi:type="array"> <item name="scope" xsi:type="string">payment</item> diff --git a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/CloseOrderTest.xml b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/CloseOrderTest.xml new file mode 100644 index 0000000000000000000000000000000000000000..30385e1cd2006c5dd86f98a8f4da1a9d032c6efd --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/CloseOrderTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * 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/etc/variations.xsd"> + <testCase name="Magento\Sales\Test\TestCase\CloseOrderTest" summary="Close order"> + <variation name="CloseOrderTestWithPayPalPaymentsPro" summary="Close order with PayPal Payments Pro" ticketId="MAGETWO-13015"> + <data name="products/0" xsi:type="string">catalogProductSimple::product_10_dollar</data> + <data name="customer/dataset" xsi:type="string">default</data> + <data name="shippingAddress/dataset" xsi:type="string">US_address_1_without_email</data> + <data name="checkoutMethod" xsi:type="string">guest</data> + <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> + <data name="shipping/shipping_method" xsi:type="string">Fixed</data> + <data name="payment/method" xsi:type="string">payflowpro</data> + <data name="prices" xsi:type="array"> + <item name="grandTotal" xsi:type="string">15.00</item> + </data> + <data name="capturedPrices" xsi:type="array"> + <item name="0" xsi:type="string">15.00</item> + </data> + <data name="creditCard/dataset" xsi:type="string">visa_default</data> + <data name="status" xsi:type="string">Complete</data> + <data name="configData" xsi:type="string">paypal_direct</data> + <data name="tag" xsi:type="string">test_type:3rd_party_test, severity:S0</data> + <constraint name="Magento\Sales\Test\Constraint\AssertCaptureInCommentsHistory" /> + <constraint name="Magento\Sales\Test\Constraint\AssertInvoiceItems" /> + <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusIsCorrect" /> + </variation> + <variation name="CloseOrderTestWithPayPalPayflowPro" summary="Close order with PayPal Payflow Pro" ticketId="MAGETWO-13020"> + <data name="products/0" xsi:type="string">catalogProductSimple::product_10_dollar</data> + <data name="customer/dataset" xsi:type="string">default</data> + <data name="shippingAddress/dataset" xsi:type="string">US_address_1_without_email</data> + <data name="checkoutMethod" xsi:type="string">guest</data> + <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> + <data name="shipping/shipping_method" xsi:type="string">Fixed</data> + <data name="payment/method" xsi:type="string">payflowpro</data> + <data name="order/data/price/dataset" xsi:type="string">full_invoice_with_product_10_dollar</data> + <data name="prices" xsi:type="array"> + <item name="grandTotal" xsi:type="string">15.00</item> + </data> + <data name="capturedPrices" xsi:type="array"> + <item name="0" xsi:type="string">15.00</item> + </data> + <data name="creditCard/dataset" xsi:type="string">visa_default</data> + <data name="status" xsi:type="string">Complete</data> + <data name="configData" xsi:type="string">payflowpro, payflowpro_use_vault</data> + <data name="tag" xsi:type="string">test_type:3rd_party_test, severity:S0</data> + <constraint name="Magento\Sales\Test\Constraint\AssertCaptureInCommentsHistory" /> + <constraint name="Magento\Sales\Test\Constraint\AssertInvoiceItems" /> + <constraint name="Magento\Sales\Test\Constraint\AssertInvoiceInInvoicesTab" /> + <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusIsCorrect" /> + </variation> + </testCase> +</config> \ No newline at end of file diff --git a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/CreateOnlineCreditMemoTest.xml b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/CreateOnlineCreditMemoTest.xml new file mode 100644 index 0000000000000000000000000000000000000000..f859f579ecea45c5c992017a09df061b11b70bbc --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/CreateOnlineCreditMemoTest.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * 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/etc/variations.xsd"> + <testCase name="Magento\Sales\Test\TestCase\CreateOnlineCreditMemoTest" summary="Create online credit memo for order placed with online payment method"> + <variation name="CreateCreditMemoPaymentsProTestVariation1" summary="Create Refund for Order Paid with PayPal Payments Pro" ticketId="MAGETWO-13059"> + <data name="products/0" xsi:type="string">catalogProductSimple::product_10_dollar</data> + <data name="customer/dataset" xsi:type="string">default</data> + <data name="checkoutMethod" xsi:type="string">guest</data> + <data name="refundedPrices" xsi:type="array"> + <item name="0" xsi:type="string">15.00</item> + </data> + <data name="shippingAddress/dataset" xsi:type="string">US_address_1_without_email</data> + <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> + <data name="shipping/shipping_method" xsi:type="string">Fixed</data> + <data name="payment/method" xsi:type="string">payflowpro</data> + <data name="configData" xsi:type="string">paypal_direct</data> + <data name="creditCard/dataset" xsi:type="string">visa_default</data> + <data name="data/items_data/0/qty" xsi:type="string">-</data> + <data name="data/form_data/do_shipment" xsi:type="string">Yes</data> + <data name="status" xsi:type="string">Closed</data> + <data name="transactions/Refund" xsi:type="array"> + <item name="transactionType" xsi:type="string">Refund</item> + <item name="statusIsClosed" xsi:type="string">Yes</item> + </data> + <data name="transactions/Capture" xsi:type="array"> + <item name="transactionType" xsi:type="string">Capture</item> + <item name="statusIsClosed" xsi:type="string">Yes</item> + </data> + <data name="tag" xsi:type="string">test_type:3rd_party_test, severity:S0</data> + <constraint name="Magento\Sales\Test\Constraint\AssertRefundSuccessCreateMessage" /> + <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusIsCorrect" /> + <constraint name="Magento\Sales\Test\Constraint\AssertRefundInCommentsHistory" /> + <constraint name="Magento\Sales\Test\Constraint\AssertRefundInCreditMemoTab" /> + <constraint name="Magento\Sales\Test\Constraint\AssertTransactionStatus" /> + </variation> + </testCase> +</config> diff --git a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/ExpressCheckoutFromProductPageTest.xml b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/ExpressCheckoutFromProductPageTest.xml index 1d92fa149efbc336eb0910a451a1740f2eda6c14..6005fe4008d7bb52308878e81a7d0211c5441a87 100644 --- a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/ExpressCheckoutFromProductPageTest.xml +++ b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/ExpressCheckoutFromProductPageTest.xml @@ -27,7 +27,7 @@ </data> <data name="payment/method" xsi:type="string">paypal_express</data> <data name="configData" xsi:type="string">paypal_express, freeshipping</data> - <data name="tag" xsi:type="string">test_type:3rd_party_test, severity:S0</data> + <data name="tag" xsi:type="string">test_type:3rd_party_test_deprecated, severity:S0</data> <constraint name="Magento\Checkout\Test\Constraint\AssertOrderSuccessPlacedMessage" /> <constraint name="Magento\Checkout\Test\Constraint\AssertMinicartEmpty" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal" /> diff --git a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/ExpressCheckoutFromShoppingCartTest.xml b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/ExpressCheckoutFromShoppingCartTest.xml index 38c876719bc3d82c772ff6f86cd507229ec5cf2b..233958afa1aa5605f922b105b2da85dee08960c8 100644 --- a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/ExpressCheckoutFromShoppingCartTest.xml +++ b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/ExpressCheckoutFromShoppingCartTest.xml @@ -28,7 +28,7 @@ <item name="grandTotal" xsi:type="string">145.98</item> </data> <data name="configData" xsi:type="string">payflowpro</data> - <data name="tag" xsi:type="string">test_type:3rd_party_test, severity:S0</data> + <data name="tag" xsi:type="string">test_type:3rd_party_test_deprecated, severity:S0</data> <constraint name="Magento\Checkout\Test\Constraint\AssertOrderSuccessPlacedMessage" /> <constraint name="Magento\Checkout\Test\Constraint\AssertMinicartEmpty" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal" /> diff --git a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/ExpressCheckoutOnePageTest.xml b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/ExpressCheckoutOnePageTest.xml index e52ed631ae46f983dc8860e7bf628f95a5a6a7fb..7de72c8ac9d93143dd3d59595a452981f2e15a89 100644 --- a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/ExpressCheckoutOnePageTest.xml +++ b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/ExpressCheckoutOnePageTest.xml @@ -26,7 +26,7 @@ </data> <data name="payment/method" xsi:type="string">paypal_express</data> <data name="configData" xsi:type="string">paypal_express</data> - <data name="tag" xsi:type="string">test_type:3rd_party_test, severity:S0</data> + <data name="tag" xsi:type="string">test_type:3rd_party_test_deprecated, severity:S0</data> <constraint name="Magento\Checkout\Test\Constraint\AssertOrderSuccessPlacedMessage" /> <constraint name="Magento\Checkout\Test\Constraint\AssertMinicartEmpty" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal" /> @@ -51,7 +51,7 @@ </data> <data name="payment/method" xsi:type="string">paypal_express</data> <data name="configData" xsi:type="string">payflowlink</data> - <data name="tag" xsi:type="string">test_type:3rd_party_test, severity:S0</data> + <data name="tag" xsi:type="string">test_type:3rd_party_test_deprecated, severity:S0</data> <constraint name="Magento\Checkout\Test\Constraint\AssertOrderSuccessPlacedMessage" /> <constraint name="Magento\Checkout\Test\Constraint\AssertMinicartEmpty" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal" /> @@ -76,7 +76,7 @@ </data> <data name="payment/method" xsi:type="string">paypal_express</data> <data name="configData" xsi:type="string">paypal_express</data> - <data name="tag" xsi:type="string">test_type:3rd_party_test</data> + <data name="tag" xsi:type="string">test_type:3rd_party_test_deprecated</data> <constraint name="Magento\Checkout\Test\Constraint\AssertOrderSuccessPlacedMessage" /> <constraint name="Magento\Checkout\Test\Constraint\AssertMinicartEmpty" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal" /> diff --git a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/OnePageCheckoutDeclinedTest.xml b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/OnePageCheckoutDeclinedTest.xml new file mode 100644 index 0000000000000000000000000000000000000000..e015e3ba566648ce05326e3fae03bf2a76eefc64 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/OnePageCheckoutDeclinedTest.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * 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/etc/variations.xsd"> + <testCase name="Magento\Checkout\Test\TestCase\OnePageCheckoutDeclinedTest" summary="Error message during OnePageCheckout"> + <variation name="OnePageCheckoutPayflowProWithAVSStreetDoesNotMatch" summary="Place Order via Payflow Pro with AVS Street verification fail" ticketId="MAGETWO-37480"> + <data name="tag" xsi:type="string">test_type:3rd_party_test, severity:S1</data> + <data name="products/0" xsi:type="string">catalogProductSimple::product_10_dollar</data> + <data name="customer/dataset" xsi:type="string">default</data> + <data name="shippingAddress/dataset" xsi:type="string">AVS_street_does_not_match_address</data> + <data name="checkoutMethod" xsi:type="string">guest</data> + <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> + <data name="shipping/shipping_method" xsi:type="string">Fixed</data> + <data name="payment/method" xsi:type="string">payflowpro</data> + <data name="creditCard/dataset" xsi:type="string">visa_default</data> + <data name="configData" xsi:type="string">payflowpro, payflowpro_avs_street_does_not_match</data> + <data name="expectedErrorMessage" xsi:type="string">An error occurred on the server. Please try to place the order again.</data> + <constraint name="Magento\Checkout\Test\Constraint\AssertCheckoutErrorMessage" /> + </variation> + </testCase> +</config> diff --git a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/OnePageCheckoutTest.xml b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/OnePageCheckoutTest.xml index 655b2f5f9d3fb83eaca544d74db4e9adf0732bbe..44577748f7b7a1abe37e6a9e562b3059820b0267 100644 --- a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/OnePageCheckoutTest.xml +++ b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/OnePageCheckoutTest.xml @@ -55,7 +55,7 @@ <constraint name="Magento\Sales\Test\Constraint\AssertAuthorizationInCommentsHistory" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderPaymentInformation" /> </variation> - <variation name="OnePageCheckoutPayflowProWithAVSStreetMatches" summary="Place Order via Payflow Pro with success AVS Street verification" ticketId="MAGETWO-37479"> + <variation name="OnePageCheckoutPayflowProWithAVSStreetMatch" summary="Place Order via Payflow Pro with success AVS Street verification" ticketId="MAGETWO-37479"> <data name="tag" xsi:type="string">test_type:3rd_party_test, severity:S1</data> <data name="products/0" xsi:type="string">catalogProductSimple::product_10_dollar</data> <data name="customer/dataset" xsi:type="string">default</data> @@ -80,5 +80,24 @@ <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusIsCorrect" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderPaymentInformation" /> </variation> + <variation name="OnePageCheckoutPayPalPaymentsPro" summary="Guest Checkout using PayPal Payments Pro and Flat Rate" ticketId="MAGETWO-62039"> + <data name="products/0" xsi:type="string">catalogProductSimple::product_10_dollar</data> + <data name="customer/dataset" xsi:type="string">default</data> + <data name="shippingAddress/dataset" xsi:type="string">US_address_1_without_email</data> + <data name="checkoutMethod" xsi:type="string">guest</data> + <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> + <data name="shipping/shipping_method" xsi:type="string">Fixed</data> + <data name="payment/method" xsi:type="string">payflowpro</data> + <data name="prices" xsi:type="array"> + <item name="grandTotal" xsi:type="string">15.00</item> + </data> + <data name="creditCard/dataset" xsi:type="string">visa_default</data> + <data name="configData" xsi:type="string">paypal_direct</data> + <data name="tag" xsi:type="string">test_type:3rd_party_test, severity:S1</data> + <constraint name="Magento\Checkout\Test\Constraint\AssertOrderSuccessPlacedMessage" /> + <constraint name="Magento\Checkout\Test\Constraint\AssertCartIsEmpty" /> + <constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal" /> + <constraint name="Magento\Sales\Test\Constraint\AssertAuthorizationInCommentsHistory" /> + </variation> </testCase> </config> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/AbstractItems.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/AbstractItems.php index d7a89b68c553808cfdbb7de9854ce37e82464618..38ad9139d4fdb2bf28c40d8420db890dcda77d3a 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/AbstractItems.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/AbstractItems.php @@ -19,63 +19,63 @@ class AbstractItems extends Block * * @var string */ - private $rowItem = 'tbody'; + protected $rowItem = 'tbody'; /** * Locator for product sku column. * * @var string */ - private $sku = '.col-product .product-sku-block'; + protected $sku = '.col-product .product-sku-block'; /** * Locator for product title column. * * @var string */ - private $title = '.col-product .product-title'; + protected $title = '.col-product .product-title'; /** * Locator for "Price" column. * * @var string */ - private $price = '.col-price .price'; + protected $price = '.col-price .price'; /** * Locator for "Qty" column. * * @var string */ - private $qty = '.col-qty'; + protected $qty = '.col-qty'; /** * Locator for "Subtotal" column. * * @var string */ - private $subtotal = '.col-subtotal .price'; + protected $subtotal = '.col-subtotal .price'; /** * Locator for "Tax Amount" column. * * @var string */ - private $taxAmount = '.col-tax .price'; + protected $taxAmount = '.col-tax .price'; /** * Locator for "Discount Amount" column. * * @var string */ - private $discountAmount = '.col-discount .price'; + protected $discountAmount = '.col-discount .price'; /** * Locator for "Row total" column. * * @var string */ - private $rowTotal = '.col-total .price'; + protected $rowTotal = '.col-total .price'; /** * Get items data. diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderStatusIsCanceled.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderStatusIsCanceled.php new file mode 100644 index 0000000000000000000000000000000000000000..2547cb49b70a447382033b2d76f7606d5545f459 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderStatusIsCanceled.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Sales\Test\Constraint; + +use Magento\Sales\Test\Page\Adminhtml\OrderIndex; +use Magento\Sales\Test\Page\Adminhtml\SalesOrderView; +use Magento\Mtf\Constraint\AbstractConstraint; + +/** + * Assert that status is Canceled. + */ +class AssertOrderStatusIsCanceled extends AbstractConstraint +{ + /** + * Assert that status is Canceled. + * + * @param OrderIndex $salesOrder + * @param SalesOrderView $salesOrderView + * @return void + */ + public function processAssert( + OrderIndex $salesOrder, + SalesOrderView $salesOrderView + ) { + $salesOrder->open(); + $grid = $salesOrder->getSalesOrderGrid(); + $grid->resetFilter(); + $grid->sortByColumn('ID'); + $grid->sortGridByField('ID'); + $grid->openFirstRow(); + + /** @var \Magento\Sales\Test\Block\Adminhtml\Order\View\Tab\Info $infoTab */ + $infoTab = $salesOrderView->getOrderForm()->openTab('info')->getTab('info'); + \PHPUnit_Framework_Assert::assertEquals( + $infoTab->getOrderStatus(), + 'Canceled' + ); + } + + /** + * Returns a string representation of the object. + * + * @return string + */ + public function toString() + { + return 'Order status is correct.'; + } +} diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Repository/OrderInjectable/Price.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/Repository/OrderInjectable/Price.xml index e9db2ef1b2ce4938732959ddea9017cf29cc54fe..64e2f8808172492ca82dc8cb545f33a0554a3fc5 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Repository/OrderInjectable/Price.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Repository/OrderInjectable/Price.xml @@ -19,6 +19,13 @@ </field> </dataset> + <dataset name="full_invoice_with_product_10_dollar"> + <field name="0" xsi:type="array"> + <item name="grand_order_total" xsi:type="string">15</item> + <item name="grand_invoice_total" xsi:type="string">15</item> + </field> + </dataset> + <dataset name="partial_invoice"> <field name="0" xsi:type="array"> <item name="grand_order_total" xsi:type="string">210</item> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CloseOrderTest.php b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CloseOrderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1594cad95eff98633a9b6b1e47a4705912059370 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CloseOrderTest.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Sales\Test\TestCase; + +use Magento\Mtf\TestCase\Scenario; + +/** + * Preconditions: + * 1. Order is placed. + * + * Steps: + * 1. Log in to Admin. + * 2. Go to Sales > Orders page. + * 3. Open order. + * 4. Click 'Ship' button and submit shipment. + * 5. Click 'Invoice' button. + * 6. Select Amount=Capture Online. + * 7. Click 'Submit Invoice' button. + * 8. Perform assertions. + * + * @group Order_Management + * @ZephyrId MAGETWO-13015, MAGETWO-13020 + */ +class CloseOrderTest extends Scenario +{ + /* tags */ + const MVP = 'yes'; + const TEST_TYPE = '3rd_party_test'; + const SEVERITY = 'S0'; + /* end tags */ + + /** + * Close order. + * + * @return void + */ + public function test() + { + $this->executeScenario(); + } +} diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOnlineCreditMemoTest.php b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOnlineCreditMemoTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e11e1391a10e3442e1a92230615d7f1c0a2fbc4d --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOnlineCreditMemoTest.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Test\TestCase; + +use Magento\Mtf\TestCase\Scenario; + +/** + * Preconditions: + * 1. Complete a sales order with online payment method. + * + * Steps: + * 1. Log in to Admin. + * 2. Open order from preconditions. + * 3. Open created invoice. + * 3. Create credit memo. + * 4. Perform assertions. + * + * @group Order_Management + * @ZephyrId MAGETWO-13059 + */ +class CreateOnlineCreditMemoTest extends Scenario +{ + /* tags */ + const MVP = 'yes'; + const TEST_TYPE = '3rd_party_test'; + const SEVERITY = 'S0'; + /* end tags */ + + /** + * Runs test for online credit memo creation for order placed with online payment method. + * + * @return void + */ + public function test() + { + $this->executeScenario(); + } +} diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/etc/di.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/etc/di.xml index 157dfc0f77302318bdce435dd3af7f6de5e261fb..e9670a1d727f72b2d7ea853fd38e4b0a2bb1c218 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/etc/di.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/etc/di.xml @@ -106,4 +106,9 @@ <argument name="severity" xsi:type="string">S1</argument> </arguments> </type> + <type name="Magento\Sales\Test\Constraint\AssertOrderStatusIsCanceled"> + <arguments> + <argument name="severity" xsi:type="string">S0</argument> + </arguments> + </type> </config> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/etc/testcase.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/etc/testcase.xml index 030b0f4f32df0eacd76c99cbe55537df1c734cf5..fa4329137b0272a5ae59cefdf217baa894c2cd28 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/etc/testcase.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/etc/testcase.xml @@ -86,4 +86,33 @@ <step name="submitOrder" module="Magento_Sales" next="createInvoice" /> <step name="createInvoice" module="Magento_Sales" /> </scenario> + <scenario name="CreateOnlineCreditMemoTest" firstStep="setupConfiguration"> + <step name="setupConfiguration" module="Magento_Config" next="createProducts" /> + <step name="createProducts" module="Magento_Catalog" next="addProductsToTheCart" /> + <step name="addProductsToTheCart" module="Magento_Checkout" next="createCustomer" /> + <step name="createCustomer" module="Magento_Customer" next="proceedToCheckout" /> + <step name="proceedToCheckout" module="Magento_Checkout" next="selectCheckoutMethod" /> + <step name="selectCheckoutMethod" module="Magento_Checkout" next="fillShippingAddress" /> + <step name="fillShippingAddress" module="Magento_Checkout" next="fillShippingMethod" /> + <step name="fillShippingMethod" module="Magento_Checkout" next="selectPaymentMethod" /> + <step name="selectPaymentMethod" module="Magento_Checkout" next="placeOrder" /> + <step name="placeOrder" module="Magento_Checkout" next="createInvoice" /> + <step name="createInvoice" module="Magento_Sales" next="createOnlineCreditMemo" /> + <step name="createOnlineCreditMemo" module="Magento_Sales" /> + </scenario> + <scenario name="CloseOrderTest" firstStep="setupConfiguration"> + <step name="setupConfiguration" module="Magento_Config" next="createProducts" /> + <step name="createProducts" module="Magento_Catalog" next="createTaxRule" /> + <step name="createTaxRule" module="Magento_Tax" next="addProductsToTheCart" /> + <step name="addProductsToTheCart" module="Magento_Checkout" next="proceedToCheckout" /> + <step name="proceedToCheckout" module="Magento_Checkout" next="createCustomer" /> + <step name="createCustomer" module="Magento_Customer" next="selectCheckoutMethod" /> + <step name="selectCheckoutMethod" module="Magento_Checkout" next="fillShippingAddress"/> + <step name="fillShippingAddress" module="Magento_Checkout" next="fillShippingMethod" /> + <step name="fillShippingMethod" module="Magento_Checkout" next="selectPaymentMethod" /> + <step name="selectPaymentMethod" module="Magento_Checkout" next="placeOrder" /> + <step name="placeOrder" module="Magento_Checkout" next="createInvoice" /> + <step name="createInvoice" module="Magento_Sales" next="createShipment" /> + <step name="createShipment" module="Magento_Sales" /> + </scenario> </config> 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 d1da95bb797dadf5d2eab2e75cce87a0f23a1b2b..be7a650d77e1c3ebe56409b5e38f9db8ef8e97ab 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 @@ -400,6 +400,8 @@ class DataGrid extends Grid } /** + * Sort grid by column. + * * @param string $columnLabel */ public function sortByColumn($columnLabel) @@ -407,6 +409,7 @@ class DataGrid extends Grid $this->waitLoader(); $this->getTemplateBlock()->waitForElementNotVisible($this->loader); $this->_rootElement->find(sprintf($this->columnHeader, $columnLabel), Locator::SELECTOR_XPATH)->click(); + $this->waitLoader(); } /** diff --git a/dev/tests/functional/utils/command.php b/dev/tests/functional/utils/command.php index 4061b07c783e4b0e8cbf4a7e4daa08d445834f68..a149be72a1ca48fbb7531b74d3dc84b687c5f737 100644 --- a/dev/tests/functional/utils/command.php +++ b/dev/tests/functional/utils/command.php @@ -8,5 +8,5 @@ if (isset($_GET['command'])) { $command = urldecode($_GET['command']); exec('php -f ../../../../bin/magento ' . $command); } else { - throw new \Exception("Command GET parameter is not set."); + throw new \InvalidArgumentException("Command GET parameter is not set."); } diff --git a/dev/tests/functional/utils/export.php b/dev/tests/functional/utils/export.php new file mode 100644 index 0000000000000000000000000000000000000000..062e8de6cbe7d6453f75d48b4faaf208ebffddc5 --- /dev/null +++ b/dev/tests/functional/utils/export.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +if (!isset($_GET['template'])) { + throw new \InvalidArgumentException('Argument "template" must be set.'); +} + +$varDir = '../../../../var/'; +$template = urldecode($_GET['template']); +$fileList = scandir($varDir, SCANDIR_SORT_NONE); +$files = []; + +foreach ($fileList as $fileName) { + if (preg_match("`$template`", $fileName) === 1) { + $filePath = $varDir . $fileName; + $files[] = [ + 'content' => file_get_contents($filePath), + 'name' => $fileName, + 'date' => filectime($filePath), + ]; + } +} + +echo serialize($files); diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php index c64500fa6cfec3aed4a33cbcbc262befa1621d5c..c2bc02893e042de06cce4023d943110a693aeeca 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php @@ -34,7 +34,7 @@ class CheckoutTest extends \PHPUnit_Framework_TestCase /** * Verify that an order placed with an existing customer can re-use the customer addresses. * - * @magentoDataFixture Magento/Paypal/_files/quote_payment_express_with_customer.php + * @magentoDataFixture Magento/Paypal/_files/quote_express_with_customer.php * @magentoAppIsolation enabled * @magentoDbIsolation enabled */ @@ -74,7 +74,7 @@ class CheckoutTest extends \PHPUnit_Framework_TestCase /** * Verify that after placing the order, addresses are associated with the order and the quote is a guest quote. * - * @magentoDataFixture Magento/Paypal/_files/quote_payment_express.php + * @magentoDataFixture Magento/Paypal/_files/quote_express.php * @magentoAppIsolation enabled * @magentoDbIsolation enabled */ diff --git a/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express.php b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express.php new file mode 100644 index 0000000000000000000000000000000000000000..0be18cdcaf9e42e2beb792596d0819b3c7a46182 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +\Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('adminhtml'); +\Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Framework\App\Config\MutableScopeConfigInterface::class +)->setValue( + 'carriers/flatrate/active', + 1, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE +); +\Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Framework\App\Config\MutableScopeConfigInterface::class +)->setValue( + 'payment/paypal_express/active', + 1, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE +); +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId('simple') + ->setId(1) + ->setAttributeSetId(4) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData( + [ + 'qty' => 100, + 'is_in_stock' => 1, + ] + )->save(); +$product->load(1); + +$billingData = [ + 'firstname' => 'testname', + 'lastname' => 'lastname', + 'company' => '', + 'email' => 'test@com.com', + 'street' => [ + 0 => 'test1', + 1 => '', + ], + 'city' => 'Test', + 'region_id' => '1', + 'region' => '', + 'postcode' => '9001', + 'country_id' => 'US', + 'telephone' => '11111111', + 'fax' => '', + 'confirm_password' => '', + 'save_in_address_book' => '1', + 'use_for_shipping' => '1', +]; + +$billingAddress = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\Quote\Address::class, ['data' => $billingData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); +$shippingAddress->setShippingMethod('flatrate_flatrate'); +$shippingAddress->setCollectShippingRates(true); + +/** @var $quote \Magento\Quote\Model\Quote */ +$quote = $objectManager->create(\Magento\Quote\Model\Quote::class); +$quote->setCustomerIsGuest( + true +)->setStoreId( + $objectManager->get( + \Magento\Store\Model\StoreManagerInterface::class + )->getStore()->getId() +)->setReservedOrderId( + '100000002' +)->setBillingAddress( + $billingAddress +)->setShippingAddress( + $shippingAddress +)->addProduct( + $product, + 10 +); +$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); +$quote->getShippingAddress()->setCollectShippingRates(true); +$quote->getPayment()->setMethod(\Magento\Paypal\Model\Config::METHOD_WPS_EXPRESS); + +$quoteRepository = $objectManager->get(\Magento\Quote\Api\CartRepositoryInterface::class); +$quoteRepository->save($quote); +$quote = $quoteRepository->get($quote->getId()); +$quote->setCustomerEmail('admin@example.com'); diff --git a/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express_with_customer.php b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express_with_customer.php new file mode 100644 index 0000000000000000000000000000000000000000..c319e298d1e50d56cdc886dd789d6c36941e8a4e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express_with_customer.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +require __DIR__ . '/../../Customer/_files/customer.php'; +require __DIR__ . '/../../Customer/_files/customer_two_addresses.php'; + +\Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('adminhtml'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$objectManager->get( + \Magento\Framework\App\Config\MutableScopeConfigInterface::class +)->setValue('carriers/flatrate/active', 1, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); +$objectManager->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class) + ->setValue('payment/paypal_express/active', 1, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); + +/** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->create(\Magento\Customer\Api\CustomerRepositoryInterface::class); +$customer = $customerRepository->getById(1); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId('simple') + ->setId(1) + ->setAttributeSetId(4) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(10) + ->setStockData([ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 100, + ]) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->save(); +$product->load(1); + +$customerBillingAddress = $objectManager->create(\Magento\Customer\Model\Address::class); +$customerBillingAddress->load(1); +$billingAddressDataObject = $customerBillingAddress->getDataModel(); +$billingAddress = $objectManager->create(\Magento\Quote\Model\Quote\Address::class); +$billingAddress->importCustomerAddressData($billingAddressDataObject); +$billingAddress->setAddressType('billing'); + +/** @var \Magento\Customer\Model\Address $customerShippingAddress */ +$customerShippingAddress = $objectManager->create(\Magento\Customer\Model\Address::class); +$customerShippingAddress->load(2); +$shippingAddressDataObject = $customerShippingAddress->getDataModel(); +$shippingAddress = $objectManager->create(\Magento\Quote\Model\Quote\Address::class); +$shippingAddress->importCustomerAddressData($shippingAddressDataObject); +$shippingAddress->setAddressType('shipping'); + +$shippingAddress->setShippingMethod('flatrate_flatrate'); +$shippingAddress->setCollectShippingRates(true); + +/** @var $quote \Magento\Quote\Model\Quote */ +$quote = $objectManager->create(\Magento\Quote\Model\Quote::class); +$quote->setCustomerIsGuest(false) + ->setCustomerId($customer->getId()) + ->setCustomer($customer) + ->setStoreId($objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore()->getId()) + ->setReservedOrderId('test02') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->addProduct($product, 10); +$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); +$quote->getShippingAddress()->setCollectShippingRates(true); +$quote->getPayment()->setMethod(\Magento\Paypal\Model\Config::METHOD_WPS_EXPRESS); + +/** @var \Magento\Quote\Api\CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->create(\Magento\Quote\Api\CartRepositoryInterface::class); +$quoteRepository->save($quote); +$quote = $quoteRepository->get($quote->getId()); diff --git a/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express.php b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express.php index fa234cc444a0730a580d35400483c0f2ee92e416..90f1102e0ec7639910c83dd76d37d0b0e36eef25 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express.php @@ -3,96 +3,7 @@ * Copyright © 2016 Magento. All rights reserved. * See COPYING.txt for license details. */ -\Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('adminhtml'); -\Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\App\Config\MutableScopeConfigInterface::class -)->setValue( - 'carriers/flatrate/active', - 1, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE -); -\Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\App\Config\MutableScopeConfigInterface::class -)->setValue( - 'payment/paypal_express/active', - 1, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE -); -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $product \Magento\Catalog\Model\Product */ -$product = $objectManager->create(\Magento\Catalog\Model\Product::class); -$product->setTypeId('simple') - ->setId(1) - ->setAttributeSetId(4) - ->setName('Simple Product') - ->setSku('simple') - ->setPrice(10) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setStockData( - [ - 'qty' => 100, - 'is_in_stock' => 1, - ] - )->save(); -$product->load(1); - -$billingData = [ - 'firstname' => 'testname', - 'lastname' => 'lastname', - 'company' => '', - 'email' => 'test@com.com', - 'street' => [ - 0 => 'test1', - 1 => '', - ], - 'city' => 'Test', - 'region_id' => '1', - 'region' => '', - 'postcode' => '9001', - 'country_id' => 'US', - 'telephone' => '11111111', - 'fax' => '', - 'confirm_password' => '', - 'save_in_address_book' => '1', - 'use_for_shipping' => '1', -]; - -$billingAddress = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Quote\Model\Quote\Address::class, ['data' => $billingData]); -$billingAddress->setAddressType('billing'); - -$shippingAddress = clone $billingAddress; -$shippingAddress->setId(null)->setAddressType('shipping'); -$shippingAddress->setShippingMethod('flatrate_flatrate'); -$shippingAddress->setCollectShippingRates(true); - -/** @var $quote \Magento\Quote\Model\Quote */ -$quote = $objectManager->create(\Magento\Quote\Model\Quote::class); -$quote->setCustomerIsGuest( - true -)->setStoreId( - $objectManager->get( - \Magento\Store\Model\StoreManagerInterface::class - )->getStore()->getId() -)->setReservedOrderId( - '100000002' -)->setBillingAddress( - $billingAddress -)->setShippingAddress( - $shippingAddress -)->addProduct( - $product, - 10 -); -$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); -$quote->getShippingAddress()->setCollectShippingRates(true); -$quote->getPayment()->setMethod(\Magento\Paypal\Model\Config::METHOD_WPS_EXPRESS); - -$quoteRepository = $objectManager->get(\Magento\Quote\Api\CartRepositoryInterface::class); -$quoteRepository->save($quote); -$quote = $quoteRepository->get($quote->getId()); -$quote->setCustomerEmail('admin@example.com'); +require __DIR__ . '/quote_express.php'; /** @var $service \Magento\Quote\Api\CartManagementInterface */ $service = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() diff --git a/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express_with_customer.php b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express_with_customer.php index 1c6793f05dea95644067521332900cc647a3648e..eaf444be13367f8769d28e34245c007122b79bf1 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express_with_customer.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express_with_customer.php @@ -3,79 +3,7 @@ * Copyright © 2016 Magento. All rights reserved. * See COPYING.txt for license details. */ - -require __DIR__ . '/../../Customer/_files/customer.php'; -require __DIR__ . '/../../Customer/_files/customer_two_addresses.php'; - -\Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('adminhtml'); - -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - -$objectManager->get( - \Magento\Framework\App\Config\MutableScopeConfigInterface::class -)->setValue('carriers/flatrate/active', 1, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); -$objectManager->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class) - ->setValue('payment/paypal_express/active', 1, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); - -/** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ -$customerRepository = $objectManager->create(\Magento\Customer\Api\CustomerRepositoryInterface::class); -$customer = $customerRepository->getById(1); - -/** @var $product \Magento\Catalog\Model\Product */ -$product = $objectManager->create(\Magento\Catalog\Model\Product::class); -$product->setTypeId('simple') - ->setId(1) - ->setAttributeSetId(4) - ->setName('Simple Product') - ->setSku('simple') - ->setPrice(10) - ->setStockData([ - 'use_config_manage_stock' => 1, - 'qty' => 100, - 'is_qty_decimal' => 0, - 'is_in_stock' => 100, -]) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->save(); -$product->load(1); - -$customerBillingAddress = $objectManager->create(\Magento\Customer\Model\Address::class); -$customerBillingAddress->load(1); -$billingAddressDataObject = $customerBillingAddress->getDataModel(); -$billingAddress = $objectManager->create(\Magento\Quote\Model\Quote\Address::class); -$billingAddress->importCustomerAddressData($billingAddressDataObject); -$billingAddress->setAddressType('billing'); - -/** @var \Magento\Customer\Model\Address $customerShippingAddress */ -$customerShippingAddress = $objectManager->create(\Magento\Customer\Model\Address::class); -$customerShippingAddress->load(2); -$shippingAddressDataObject = $customerShippingAddress->getDataModel(); -$shippingAddress = $objectManager->create(\Magento\Quote\Model\Quote\Address::class); -$shippingAddress->importCustomerAddressData($shippingAddressDataObject); -$shippingAddress->setAddressType('shipping'); - -$shippingAddress->setShippingMethod('flatrate_flatrate'); -$shippingAddress->setCollectShippingRates(true); - -/** @var $quote \Magento\Quote\Model\Quote */ -$quote = $objectManager->create(\Magento\Quote\Model\Quote::class); -$quote->setCustomerIsGuest(false) - ->setCustomerId($customer->getId()) - ->setCustomer($customer) - ->setStoreId($objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore()->getId()) - ->setReservedOrderId('test02') - ->setBillingAddress($billingAddress) - ->setShippingAddress($shippingAddress) - ->addProduct($product, 10); -$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); -$quote->getShippingAddress()->setCollectShippingRates(true); -$quote->getPayment()->setMethod(\Magento\Paypal\Model\Config::METHOD_WPS_EXPRESS); - -/** @var \Magento\Quote\Api\CartRepositoryInterface $quoteRepository */ -$quoteRepository = $objectManager->create(\Magento\Quote\Api\CartRepositoryInterface::class); -$quoteRepository->save($quote); -$quote = $quoteRepository->get($quote->getId()); +require __DIR__ . '/quote_express_with_customer.php'; /** @var $service \Magento\Quote\Api\CartManagementInterface */ $service = $objectManager->create(\Magento\Quote\Api\CartManagementInterface::class); diff --git a/dev/tests/js/jasmine/require.conf.js b/dev/tests/js/jasmine/require.conf.js index 9ba59b81bc27f5326673e59270917b232643ee24..c60ec02943b5ba86a0c7622bf64ce1c037351805 100644 --- a/dev/tests/js/jasmine/require.conf.js +++ b/dev/tests/js/jasmine/require.conf.js @@ -16,7 +16,13 @@ require.config({ ] }, paths: { - 'tests': 'dev/tests/js/jasmine' + 'tests': 'dev/tests/js/jasmine', + 'squire': 'node_modules/squirejs/src/Squire' + }, + shim: { + squire: { + exports: 'squire' + } }, config: { jsbuild: { diff --git a/dev/tests/js/jasmine/spec_runner/settings.json b/dev/tests/js/jasmine/spec_runner/settings.json index 109da479146da84882c4a6a68e8778db0b19701d..25407123c1f34d66c70c4406d9341f4659f7519b 100644 --- a/dev/tests/js/jasmine/spec_runner/settings.json +++ b/dev/tests/js/jasmine/spec_runner/settings.json @@ -57,7 +57,8 @@ "^\/_SpecRunner.html", "^\/dev\/tests", "^\/.grunt", - "^\/pub\/static" + "^\/pub\/static", + "^\/node_modules" ], "options": { /** diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/new-customer-address.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/new-customer-address.test.js new file mode 100644 index 0000000000000000000000000000000000000000..ae7ff3d145e2b0ae8c2850ff8d2217baa9fed3f1 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/new-customer-address.test.js @@ -0,0 +1,74 @@ +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Checkout/js/model/new-customer-address' +], function (NewCustomerAddress) { + 'use strict'; + + describe('Magento_Checkout/js/model/new-customer-address', function () { + var newCustomerAddress; + + window.checkoutConfig = { + defaultCountryId: 'US' + }; + + beforeEach(function () { + newCustomerAddress = NewCustomerAddress; + }); + + it('Check that is executable.', function () { + expect(typeof newCustomerAddress).toEqual('function'); + }); + + it('Check on empty object.', function () { + var expected = { + countryId: 'US', + regionCode: null, + region: null + }; + + expect(JSON.stringify(newCustomerAddress({}))).toEqual(JSON.stringify(expected)); + }); + + it('Check on function call with empty address data.', function () { + var result = newCustomerAddress({}); + + expect(result.isDefaultShipping()).toBeUndefined(); + expect(result.isDefaultBilling()).toBeUndefined(); + expect(result.getType()).toEqual('new-customer-address'); + expect(result.getKey()).toEqual('new-customer-address'); + expect(result.getKey()).toContain('new-customer-address'); + expect(result.isEditable()).toBeTruthy(); + expect(result.canUseForBilling()).toBeTruthy(); + }); + + it('Check on regionId with region object in address data.', function () { + var result = newCustomerAddress({ + region: { + 'region_id': 1 + } + }), + expected = { + countryId: 'US', + regionId: 1 + }; + + expect(JSON.stringify(result)).toEqual(JSON.stringify(expected)); + }); + it('Check on regionId with countryId in address data.', function () { + var result = newCustomerAddress({ + 'country_id': 'US' + }), + expected = { + countryId: 'US', + regionCode: null, + region: null + }; + + expect(JSON.stringify(result)).toEqual(JSON.stringify(expected)); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/view/cart/shipping-estimation.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/view/cart/shipping-estimation.test.js new file mode 100644 index 0000000000000000000000000000000000000000..785d88d81e3543aae46ad1564c7a4ba5edbcba5c --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/view/cart/shipping-estimation.test.js @@ -0,0 +1,115 @@ +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +/* eslint max-nested-callbacks: 0 */ + +define(['squire', 'ko'], function (Squire, ko) { + 'use strict'; + + var injector = new Squire(), + checkoutProvider = { + on: jasmine.createSpy() + }, + mocks = { + 'Magento_Checkout/js/action/select-shipping-address': jasmine.createSpy(), + 'Magento_Checkout/js/model/address-converter': { + formAddressDataToQuoteAddress: jasmine.createSpy() + }, + 'Magento_Checkout/js/model/cart/estimate-service': jasmine.createSpy(), + 'Magento_Checkout/js/checkout-data': jasmine.createSpy(), + 'Magento_Checkout/js/model/shipping-rates-validator': { + bindChangeHandlers: jasmine.createSpy() + }, + 'uiRegistry': { + async: jasmine.createSpy().and.returnValue(function (callback) { + callback(checkoutProvider); + }), + create: jasmine.createSpy(), + get: jasmine.createSpy(), + set: jasmine.createSpy() + }, + 'Magento_Checkout/js/model/quote': { + isVirtual: jasmine.createSpy(), + shippingAddress: jasmine.createSpy() + }, + 'Magento_Checkout/js/model/checkout-data-resolver': { + resolveEstimationAddress: jasmine.createSpy() + }, + 'mage/validation': jasmine.createSpy() + }, + obj; + + beforeEach(function (done) { + injector.mock(mocks); + injector.require(['Magento_Checkout/js/view/cart/shipping-estimation'], function (Constr) { + obj = new Constr({ + provider: 'provName', + name: '', + index: '' + }); + done(); + }); + }); + + describe('Magento_Checkout/js/view/cart/shipping-estimation', function () { + describe('"initElement" method', function () { + it('Check for return value and element that initiated.', function () { + var element = jasmine.createSpyObj('element', ['initContainer']); + + expect(obj.initElement(element)).toBe(obj); + expect(mocks['Magento_Checkout/js/model/shipping-rates-validator'].bindChangeHandlers) + .not.toHaveBeenCalled(); + }); + it('Check shipping rates validator call.', function () { + var element = { + index: 'address-fieldsets', + elems: ko.observable(), + initContainer: jasmine.createSpy() + }; + + spyOn(element.elems, 'subscribe'); + + obj.initElement(element); + expect(mocks['Magento_Checkout/js/model/shipping-rates-validator'].bindChangeHandlers) + .toHaveBeenCalledWith(element.elems(), true, 500); + expect(element.elems.subscribe) + .toHaveBeenCalledWith(jasmine.any(Function)); + }); + }); + + describe('"getEstimationInfo" method', function () { + it('Check for invalid form data.', function () { + obj.source = { + get: jasmine.createSpy().and.returnValue(true), + set: jasmine.createSpy(), + trigger: jasmine.createSpy() + }; + + expect(obj.getEstimationInfo()).toBeUndefined(); + expect(obj.source.get).toHaveBeenCalledWith('params.invalid'); + expect(obj.source.get).not.toHaveBeenCalledWith('shippingAddress'); + expect(obj.source.set).toHaveBeenCalledWith('params.invalid', false); + expect(obj.source.trigger).toHaveBeenCalledWith('shippingAddress.data.validate'); + expect(mocks['Magento_Checkout/js/action/select-shipping-address']).not.toHaveBeenCalled(); + obj.source = {}; + }); + it('Check for vaild form data.', function () { + obj.source = { + get: jasmine.createSpy().and.returnValues(false, {}), + set: jasmine.createSpy(), + trigger: jasmine.createSpy() + }; + + expect(obj.getEstimationInfo()).toBeUndefined(); + expect(obj.source.get).toHaveBeenCalledWith('params.invalid'); + expect(obj.source.get).toHaveBeenCalledWith('shippingAddress'); + expect(obj.source.set).toHaveBeenCalledWith('params.invalid', false); + expect(obj.source.trigger).toHaveBeenCalledWith('shippingAddress.data.validate'); + expect(mocks['Magento_Checkout/js/action/select-shipping-address']).toHaveBeenCalled(); + obj.source = {}; + }); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/view/shipping.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/view/shipping.test.js new file mode 100644 index 0000000000000000000000000000000000000000..20f3b2a090d7a38882a0728be94998ea7c359c70 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/view/shipping.test.js @@ -0,0 +1,194 @@ +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +/* eslint max-nested-callbacks: 0 */ +require.config({ + map: { + '*': { + 'Magento_Checkout/js/view/shipping': 'Magento_Checkout/js/view/shipping' + } + } +}); + +define(['squire', 'ko', 'jquery', 'jquery/validate'], function (Squire, ko, $) { + 'use strict'; + + var injector = new Squire(), + modalStub = { + openModal: jasmine.createSpy(), + closeModal: jasmine.createSpy() + }, + mocks = { + 'Magento_Customer/js/model/customer': { + isLoggedIn: ko.observable() + }, + 'Magento_Customer/js/model/address-list': ko.observableArray(), + 'Magento_Checkout/js/model/address-converter': jasmine.createSpy(), + 'Magento_Checkout/js/model/quote': { + isVirtual: jasmine.createSpy(), + shippingMethod: ko.observable() + }, + 'Magento_Checkout/js/action/create-shipping-address': jasmine.createSpy().and.returnValue( + jasmine.createSpyObj('newShippingAddress', ['getKey']) + ), + 'Magento_Checkout/js/action/select-shipping-address': jasmine.createSpy(), + 'Magento_Checkout/js/model/shipping-rates-validator': jasmine.createSpy(), + 'Magento_Checkout/js/model/shipping-address/form-popup-state': { + isVisible: ko.observable() + }, + 'Magento_Checkout/js/model/shipping-service': jasmine.createSpyObj('service', ['getShippingRates']), + 'Magento_Checkout/js/action/select-shipping-method': jasmine.createSpy(), + 'Magento_Checkout/js/model/shipping-rate-registry': jasmine.createSpy(), + 'Magento_Checkout/js/action/set-shipping-information': jasmine.createSpy(), + 'Magento_Checkout/js/model/step-navigator': jasmine.createSpyObj('navigator', ['registerStep']), + 'Magento_Ui/js/modal/modal': jasmine.createSpy('modal').and.returnValue(modalStub), + 'Magento_Checkout/js/model/checkout-data-resolver': jasmine.createSpyObj( + 'dataResolver', + ['resolveShippingAddress'] + ), + 'Magento_Checkout/js/checkout-data': jasmine.createSpyObj( + 'checkoutData', + ['setSelectedShippingAddress', 'setNewCustomerShippingAddress', 'setSelectedShippingRate'] + ), + 'uiRegistry': jasmine.createSpy(), + 'Magento_Checkout/js/model/shipping-rate-service': jasmine.createSpy() + }, + obj; + + beforeEach(function (done) { + injector.mock(mocks); + injector.require(['Magento_Checkout/js/view/shipping'], function (Constr) { + obj = new Constr({ + provider: 'provName', + name: '', + index: '', + popUpForm: { + options: { + buttons: { + save: {}, + cancel: {} + } + } + } + }); + done(); + }); + }); + + describe('Magento_Checkout/js/view/shipping', function () { + describe('"navigate" method', function () { + it('Check for return value.', function () { + expect(obj.navigate()).toBeUndefined(); + }); + }); + + describe('"getPopUp" method', function () { + it('Check for return value.', function () { + expect(obj.getPopUp()).toBe(modalStub); + expect(mocks['Magento_Ui/js/modal/modal']).toHaveBeenCalled(); + mocks['Magento_Ui/js/modal/modal'].calls.reset(); + }); + it('Check on single modal call', function () { + expect(obj.getPopUp()).toBe(modalStub); + expect(mocks['Magento_Ui/js/modal/modal']).not.toHaveBeenCalled(); + }); + }); + + describe('"showFormPopUp" method', function () { + it('Check method call.', function () { + expect(obj.showFormPopUp()).toBeUndefined(); + expect(obj.isFormPopUpVisible()).toBeTruthy(); + expect(modalStub.openModal).toHaveBeenCalled(); + }); + }); + + describe('"saveNewAddress" method', function () { + it('Check method call with invalid form data.', function () { + obj.source = { + get: jasmine.createSpy().and.returnValue(true), + set: jasmine.createSpy(), + trigger: jasmine.createSpy() + }; + + expect(obj.saveNewAddress()).toBeUndefined(); + expect(obj.isNewAddressAdded()).toBeFalsy(); + expect(modalStub.closeModal).not.toHaveBeenCalled(); + }); + it('Check method call with valid form data.', function () { + obj.source = { + get: jasmine.createSpy().and.returnValues(true, false, {}), + set: jasmine.createSpy(), + trigger: jasmine.createSpy() + }; + + expect(obj.saveNewAddress()).toBeUndefined(); + expect(obj.isNewAddressAdded()).toBeTruthy(); + expect(modalStub.closeModal).toHaveBeenCalled(); + }); + }); + + describe('"selectShippingMethod" method', function () { + it('Check method call.', function () { + var shippingMethod = { + 'carrier_code': 'carrier', + 'method_code': 'method' + }; + + expect(obj.selectShippingMethod(shippingMethod)).toBeTruthy(); + expect(mocks['Magento_Checkout/js/checkout-data'].setSelectedShippingRate) + .toHaveBeenCalledWith('carrier_method'); + }); + }); + + describe('"setShippingInformation" method', function () { + it('Check method call.', function () { + expect(obj.setShippingInformation()).toBeUndefined(); + }); + }); + + describe('"validateShippingInformation" method', function () { + it('Check method call on negative cases.', function () { + obj.source = { + get: jasmine.createSpy().and.returnValue(true), + set: jasmine.createSpy(), + trigger: jasmine.createSpy() + }; + + expect(obj.validateShippingInformation()).toBeFalsy(); + expect(obj.errorValidationMessage()).toBe('Please specify a shipping method.'); + spyOn(mocks['Magento_Checkout/js/model/quote'], 'shippingMethod').and.returnValue(true); + spyOn(mocks['Magento_Customer/js/model/customer'], 'isLoggedIn').and.returnValue(true); + expect(obj.validateShippingInformation()).toBeFalsy(); + }); + it('Check method call on positive case.', function () { + $('body').append('<form data-role="email-with-possible-login">' + + '<input type="text" name="username" />' + + '</form>'); + obj.source = { + get: jasmine.createSpy().and.returnValue(true), + set: jasmine.createSpy(), + trigger: jasmine.createSpy() + }; + obj.isFormInline = false; + + spyOn(mocks['Magento_Checkout/js/model/quote'], 'shippingMethod').and.returnValue(true); + spyOn(mocks['Magento_Customer/js/model/customer'], 'isLoggedIn').and.returnValue(false); + spyOn($.fn, 'valid').and.returnValue(true); + expect(obj.validateShippingInformation()).toBeTruthy(); + }); + }); + + describe('"triggerShippingDataValidateEvent" method', function () { + it('Check method call.', function () { + obj.source = { + get: jasmine.createSpy().and.returnValue(true), + set: jasmine.createSpy(), + trigger: jasmine.createSpy() + }; + expect(obj.triggerShippingDataValidateEvent()).toBeUndefined(); + }); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/model/customer/address.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/model/customer/address.test.js new file mode 100644 index 0000000000000000000000000000000000000000..0384c9df2c583c31ad3f95a6b942b8c73b0b3af5 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/model/customer/address.test.js @@ -0,0 +1,58 @@ +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Customer/js/model/customer/address' +], function (CustomerAddress) { + 'use strict'; + + describe('Magento_Customer/js/model/customer/address', function () { + var customerAddress; + + beforeEach(function () { + customerAddress = CustomerAddress; + }); + + it('Check that is executable.', function () { + expect(typeof customerAddress).toEqual('function'); + }); + + it('Check on empty object.', function () { + var addressData = { + region: {} + }; + + expect(JSON.stringify(customerAddress(addressData))).toEqual(JSON.stringify({})); + }); + + it('Check on function call with empty address data.', function () { + var result = customerAddress({ + region: {} + }); + + expect(result.isDefaultShipping()).toBeUndefined(); + expect(result.isDefaultBilling()).toBeUndefined(); + expect(result.getAddressInline()).toBeUndefined(); + expect(result.getType()).toEqual('customer-address'); + expect(result.getKey()).toContain('customer-address'); + expect(result.getCacheKey()).toContain('customer-address'); + expect(result.isEditable()).toBeFalsy(); + expect(result.canUseForBilling()).toBeTruthy(); + }); + + it('Check on regionId with region object in address data.', function () { + var result = customerAddress({ + region: { + 'region_id': 1 + } + }), + expected = { + regionId: '1' + }; + + expect(JSON.stringify(result)).toEqual(JSON.stringify(expected)); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/view/authentication-popup.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/view/authentication-popup.test.js new file mode 100644 index 0000000000000000000000000000000000000000..128bd86edbf990cc28002152ac128b134c0488be --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/view/authentication-popup.test.js @@ -0,0 +1,81 @@ +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +/* eslint max-nested-callbacks: 0 */ +define(['squire'], function (Squire) { + 'use strict'; + + var injector = new Squire(), + loginAction = jasmine.createSpy(), + mocks = { + 'Magento_Customer/js/action/login': loginAction, + 'Magento_Customer/js/customer-data': { + get: jasmine.createSpy() + }, + 'Magento_Customer/js/model/authentication-popup': { + createPopUp: jasmine.createSpy(), + modalWindow: null + }, + 'Magento_Ui/js/modal/alert': jasmine.createSpy(), + 'mage/url': jasmine.createSpyObj('customerData', ['setBaseUrl']) + }, + obj; + + loginAction.registerLoginCallback = jasmine.createSpy(); + window.authenticationPopup = { + customerRegisterUrl: 'register_url', + customerForgotPasswordUrl: 'forgot_password_url', + autocomplete: 'autocomplete_flag', + baseUrl: 'base_url' + }; + + beforeEach(function (done) { + injector.mock(mocks); + injector.require(['Magento_Customer/js/view/authentication-popup'], function (Constr) { + obj = new Constr({ + provider: 'provName', + name: '', + index: '' + }); + done(); + }); + }); + + describe('Magento_Customer/js/view/authentication-popup', function () { + describe('"isActive" method', function () { + it('Check for return value.', function () { + mocks['Magento_Customer/js/customer-data'].get.and.returnValue(function () { + return true; + }); + expect(obj.isActive()).toBeFalsy(); + }); + }); + }); + + describe('Magento_Customer/js/view/authentication-popup', function () { + describe('"setModalElement" method', function () { + it('Check for return value.', function () { + expect(obj.setModalElement()).toBeUndefined(); + expect(mocks['Magento_Customer/js/model/authentication-popup'].createPopUp).toHaveBeenCalled(); + }); + }); + }); + + describe('Magento_Customer/js/view/authentication-popup', function () { + describe('"login" method', function () { + it('Check for return value.', function () { + var event = { + currentTarget: '<form><input type="text" name="username" value="customer"/></form>', + stopPropagation: jasmine.createSpy() + }; + + expect(obj.login(null, event)).toBeFalsy(); + expect(mocks['Magento_Customer/js/action/login']).toHaveBeenCalledWith({ + username: 'customer' + }); + }); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/paging/paging.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/paging/paging.test.js index a240e5cfd432597d4bb38e10957b4495d4ed48b2..d6480261c3fe14fe3dca807bec1375d3947972fe 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/paging/paging.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/paging/paging.test.js @@ -29,13 +29,10 @@ define([ it('normal + boundary values', function () { expect(paging.normalize(1)).toBe(1); - expect(paging.normalize(2)).toBe(2); - expect(paging.normalize(4)).toBe(4); }); it('out of boundary values', function () { expect(paging.normalize(0)).toBe(1); - expect(paging.normalize(5)).toBe(4); }); }); diff --git a/package.json.sample b/package.json.sample index 40169b3179052d98ad2c1a79baf48e376689f3c1..d73606809abe6563a57c562a27544def4ddce84a 100644 --- a/package.json.sample +++ b/package.json.sample @@ -36,7 +36,8 @@ "serve-static": "^1.7.1", "strip-json-comments": "^1.0.2", "time-grunt": "^1.0.0", - "underscore": "^1.7.0" + "underscore": "^1.7.0", + "squirejs": "0.2.1" }, "engines": { "node": ">=0.10.0"