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/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/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/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/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; + } +}