diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php index b0d218dc2e285627ec835808ba4e505fd3c593aa..95534bd114b49af653e56df164fdd7d21fcfc419 100644 --- a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php +++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php @@ -203,17 +203,19 @@ class BundlePanel extends AbstractModifier $pricePath = $this->arrayManager->slicePath($pricePath, 0, -1) . '/value_type/arguments/data/options'; $price = $this->arrayManager->get($pricePath, $meta); - $meta = $this->arrayManager->remove($pricePath, $meta); - foreach ($price as $key => $item) { - if ($item['value'] == ProductPriceOptionsInterface::VALUE_FIXED) { - unset($price[$key]); + if ($price) { + $meta = $this->arrayManager->remove($pricePath, $meta); + foreach ($price as $key => $item) { + if ($item['value'] == ProductPriceOptionsInterface::VALUE_FIXED) { + unset($price[$key]); + } } + $meta = $this->arrayManager->merge( + $this->arrayManager->slicePath($pricePath, 0, -1), + $meta, + ['options' => $price] + ); } - $meta = $this->arrayManager->merge( - $this->arrayManager->slicePath($pricePath, 0, -1), - $meta, - ['options' => $price] - ); return $meta; } diff --git a/app/code/Magento/Bundle/etc/di.xml b/app/code/Magento/Bundle/etc/di.xml index 6afd212e1b82bf7487076941893fbbc10a33f8a3..e2a913acd50b783f39f2b75dbe9bfb297727e6ba 100644 --- a/app/code/Magento/Bundle/etc/di.xml +++ b/app/code/Magento/Bundle/etc/di.xml @@ -130,5 +130,11 @@ </argument> </arguments> </type> - + <type name="Magento\Catalog\Model\Product\Price\SpecialPriceStorage"> + <arguments> + <argument name="allowedProductTypes" xsi:type="array"> + <item name="2" xsi:type="string">bundle</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Catalog/Api/BasePriceStorageInterface.php b/app/code/Magento/Catalog/Api/BasePriceStorageInterface.php index b669e5a1e8ce80a3eccaf99078d1faf2c84adfcb..399ff340146788f40b8ba209fa5cd36781e7f8ce 100644 --- a/app/code/Magento/Catalog/Api/BasePriceStorageInterface.php +++ b/app/code/Magento/Catalog/Api/BasePriceStorageInterface.php @@ -13,18 +13,25 @@ namespace Magento\Catalog\Api; interface BasePriceStorageInterface { /** - * Return product prices. + * Return product prices. In case of at least one of skus is not found exception will be thrown. * * @param string[] $skus * @return \Magento\Catalog\Api\Data\BasePriceInterface[] + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function get(array $skus); /** * Add or update product prices. + * Input item should correspond \Magento\Catalog\Api\Data\CostInterface. + * If any items will have invalid price, store id or sku, they will be marked as failed and excluded from + * update list and \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] with problem description will be returned. + * If there were no failed items during update empty array will be returned. + * If error occurred during the update exception will be thrown. * * @param \Magento\Catalog\Api\Data\BasePriceInterface[] $prices - * @return bool Will returned True if updated. + * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] + * @throws \Magento\Framework\Exception\CouldNotSaveException */ public function update(array $prices); } diff --git a/app/code/Magento/Catalog/Api/CostStorageInterface.php b/app/code/Magento/Catalog/Api/CostStorageInterface.php index 31055227ad1f2e4de488a7b70dea79eac52d0bb9..94a5c42732a2a1e63718af8d170dd1c7b442b7d1 100644 --- a/app/code/Magento/Catalog/Api/CostStorageInterface.php +++ b/app/code/Magento/Catalog/Api/CostStorageInterface.php @@ -13,26 +13,34 @@ namespace Magento\Catalog\Api; interface CostStorageInterface { /** - * Return product prices. + * Return product prices. In case of at least one of skus is not found exception will be thrown. * * @param string[] $skus * @return \Magento\Catalog\Api\Data\CostInterface[] + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function get(array $skus); /** * Add or update product cost. + * Input item should correspond to \Magento\Catalog\Api\Data\CostInterface. + * If any items will have invalid cost, store id or sku, they will be marked as failed and excluded from + * update list and \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] with problem description will be returned. + * If there were no failed items during update empty array will be returned. + * If error occurred during the update exception will be thrown. * * @param \Magento\Catalog\Api\Data\CostInterface[] $prices - * @return bool Will returned True if updated. + * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] */ public function update(array $prices); /** - * Delete product cost. + * Delete product cost. In case of at least one of skus is not found exception will be thrown. + * If error occurred during the delete exception will be thrown. * * @param string[] $skus - * @return bool Will returned True if deleted. + * @return bool Will return True if deleted. + * @throws \Magento\Framework\Exception\NoSuchEntityException * @throws \Magento\Framework\Exception\CouldNotDeleteException */ public function delete(array $skus); diff --git a/app/code/Magento/Catalog/Api/Data/PriceUpdateResultInterface.php b/app/code/Magento/Catalog/Api/Data/PriceUpdateResultInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..216f649efe86d7e5d2b1cf3245f8a4f4bfd49ffd --- /dev/null +++ b/app/code/Magento/Catalog/Api/Data/PriceUpdateResultInterface.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Api\Data; + +/** + * Interface returned in case of incorrect price passed to efficient price API. + * @api + */ +interface PriceUpdateResultInterface extends \Magento\Framework\Api\ExtensibleDataInterface +{ + /**#@+ + * Constants + */ + const MESSAGE = 'message'; + const PARAMETERS = 'parameters'; + /**#@-*/ + + /** + * Get error message, that contains description of error occurred during price update. + * + * @return string + */ + public function getMessage(); + + /** + * Set error message, that contains description of error occurred during price update. + * + * @param string $message + * @return $this + */ + public function setMessage($message); + + /** + * Get parameters, that could be displayed in error message placeholders. + * + * @return string[] + */ + public function getParameters(); + + /** + * Set parameters, that could be displayed in error message placeholders. + * + * @param string[] $parameters + * @return $this + */ + public function setParameters(array $parameters); + + /** + * Retrieve existing extension attributes object. + * If extension attributes do not exist return null. + * + * @return \Magento\Catalog\Api\Data\PriceUpdateResultExtensionInterface|null + */ + public function getExtensionAttributes(); + + /** + * Set an extension attributes object. + * + * @param \Magento\Catalog\Api\Data\PriceUpdateResultExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Magento\Catalog\Api\Data\PriceUpdateResultExtensionInterface $extensionAttributes + ); +} diff --git a/app/code/Magento/Catalog/Api/Data/SpecialPriceInterface.php b/app/code/Magento/Catalog/Api/Data/SpecialPriceInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..30f6b65106ac4dced8750bbfd3226a14a6c84ea1 --- /dev/null +++ b/app/code/Magento/Catalog/Api/Data/SpecialPriceInterface.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Api\Data; + +/** + * Product Special Price Interface is used to encapsulate data that can be processed by efficient price API. + * @api + */ +interface SpecialPriceInterface extends \Magento\Framework\Api\ExtensibleDataInterface +{ + /**#@+ + * Constants + */ + const PRICE = 'price'; + const STORE_ID = 'store_id'; + const SKU = 'sku'; + const PRICE_FROM = 'price_from'; + const PRICE_TO = 'price_to'; + /**#@-*/ + + /** + * Set product special price value. + * + * @param float $price + * @return $this + */ + public function setPrice($price); + + /** + * Get product special price value. + * + * @return float + */ + public function getPrice(); + + /** + * Set ID of store, that contains special price value. + * + * @param int $storeId + * @return $this + */ + public function setStoreId($storeId); + + /** + * Get ID of store, that contains special price value. + * + * @return int + */ + public function getStoreId(); + + /** + * Set SKU of product, that contains special price value. + * + * @param string $sku + * @return $this + */ + public function setSku($sku); + + /** + * Get SKU of product, that contains special price value. + * + * @return string + */ + public function getSku(); + + /** + * Set start date for special price in Y-m-d H:i:s format. + * + * @param string $datetime + * @return $this + */ + public function setPriceFrom($datetime); + + /** + * Get start date for special price in Y-m-d H:i:s format. + * + * @return string + */ + public function getPriceFrom(); + + /** + * Set end date for special price in Y-m-d H:i:s format. + * + * @param string $datetime + * @return $this + */ + public function setPriceTo($datetime); + + /** + * Get end date for special price in Y-m-d H:i:s format. + * + * @return string + */ + public function getPriceTo(); + + /** + * Retrieve existing extension attributes object. + * If extension attributes do not exist return null. + * + * @return \Magento\Catalog\Api\Data\SpecialPriceExtensionInterface|null + */ + public function getExtensionAttributes(); + + /** + * Set an extension attributes object. + * + * @param \Magento\Catalog\Api\Data\SpecialPriceExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Magento\Catalog\Api\Data\SpecialPriceExtensionInterface $extensionAttributes + ); +} diff --git a/app/code/Magento/Catalog/Api/SpecialPriceInterface.php b/app/code/Magento/Catalog/Api/SpecialPriceInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..971656da1fbfc97bca76d4f576bf540e7350df47 --- /dev/null +++ b/app/code/Magento/Catalog/Api/SpecialPriceInterface.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Api; + +/** + * Special prices resource model. + * @api + */ +interface SpecialPriceInterface +{ + /** + * Get product special prices by SKUs. + * + * @param string[] $skus Array containing SKUs + * $skus = [ + * 'sku value 1', + * 'sku value 2' + * ]; + * @return [ + * 'entity_id' => (int) Entity identified or entity link field. + * 'value' => (float) Special price value. + * 'store_id' => (int) Store Id. + * 'sku' => (string) Product SKU. + * 'price_from' => (string) Special price from date value in UTC. + * 'price_to' => (string) Special price to date value in UTC. + * ] + */ + public function get(array $skus); + + /** + * Update product special prices. + * + * @param array $prices + * $prices = [ + * 'entity_id' => (int) Entity identified or entity link field. Required. + * 'attribute_id' => (int) Special price attribute Id. Required. + * 'store_id' => (int) Store Id. Required. + * 'value' => (float) Special price value. Required. + * 'price_from' => (string) Special price from date value in Y-m-d H:i:s format in UTC. Optional. + * 'price_to' => (string) Special price to date value in Y-m-d H:i:s format in UTC. Optional. + * ]; + * @return bool + * @throws \Magento\Framework\Exception\CouldNotSaveException Thrown if error occurred during price save. + */ + public function update(array $prices); + + /** + * Delete product special prices. + * + * @param array $prices + * $prices = [ + * 'entity_id' => (int) Entity identified or entity link field. Required. + * 'attribute_id' => (int) Special price attribute Id. Required. + * 'store_id' => (int) Store Id. Required. + * 'value' => (float) Special price value. Required. + * 'price_from' => (string) Special price from date value in Y-m-d H:i:s format in UTC. Optional. + * 'price_to' => (string) Special price to date value in Y-m-d H:i:s format in UTC. Optional. + * ]; + * @return bool + * @throws \Magento\Framework\Exception\CouldNotDeleteException Thrown if error occurred during price delete. + */ + public function delete(array $prices); +} diff --git a/app/code/Magento/Catalog/Api/SpecialPriceStorageInterface.php b/app/code/Magento/Catalog/Api/SpecialPriceStorageInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..b59880f0ed93dd8a510ed00f04bcbe8cd6b0f5e5 --- /dev/null +++ b/app/code/Magento/Catalog/Api/SpecialPriceStorageInterface.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Api; + +/** + * Special price storage presents efficient price API and is used to retrieve, update or delete special prices. + * @api + */ +interface SpecialPriceStorageInterface +{ + /** + * Return product's special price. In case of at least one of skus is not found exception will be thrown. + * + * @param string[] $skus + * @return \Magento\Catalog\Api\Data\SpecialPriceInterface[] + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function get(array $skus); + + /** + * Add or update product's special price. + * If any items will have invalid price, store id, sku or dates, they will be marked as failed and excluded from + * update list and \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] with problem description will be returned. + * If there were no failed items during update empty array will be returned. + * If error occurred during the update exception will be thrown. + * + * @param \Magento\Catalog\Api\Data\SpecialPriceInterface[] $prices + * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] + * @throws \Magento\Framework\Exception\CouldNotSaveException + */ + public function update(array $prices); + + /** + * Delete product's special price. + * If any items will have invalid price, store id, sku or dates, they will be marked as failed and excluded from + * delete list and \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] with problem description will be returned. + * If there were no failed items during update empty array will be returned. + * If error occurred during the delete exception will be thrown. + * + * @param \Magento\Catalog\Api\Data\SpecialPriceInterface[] $prices + * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] + * @throws \Magento\Framework\Exception\CouldNotDeleteException + */ + public function delete(array $prices); +} diff --git a/app/code/Magento/Catalog/Api/TierPriceStorageInterface.php b/app/code/Magento/Catalog/Api/TierPriceStorageInterface.php index 14eed58ef3359a1595f16e72465383a142b7d77a..d23eb637dbf26217c1d93338835f6a8f2862aa98 100644 --- a/app/code/Magento/Catalog/Api/TierPriceStorageInterface.php +++ b/app/code/Magento/Catalog/Api/TierPriceStorageInterface.php @@ -13,34 +13,50 @@ namespace Magento\Catalog\Api; interface TierPriceStorageInterface { /** - * Return product prices. + * Return product prices. In case of at least one of skus is not found exception will be thrown. * * @param string[] $skus * @return \Magento\Catalog\Api\Data\TierPriceInterface[] + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function get(array $skus); /** * Add or update product prices. + * If any items will have invalid price, price type, website id, sku, customer group or quantity, they will be + * marked as failed and excluded from update list and \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] + * with problem description will be returned. + * If there were no failed items during update empty array will be returned. + * If error occurred during the update exception will be thrown. * * @param \Magento\Catalog\Api\Data\TierPriceInterface[] $prices - * @return bool Will returned True if updated. + * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] */ public function update(array $prices); /** * Remove existing tier prices and replace them with the new ones. + * If any items will have invalid price, price type, website id, sku, customer group or quantity, they will be + * marked as failed and excluded from replace list and \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] + * with problem description will be returned. + * If there were no failed items during update empty array will be returned. + * If error occurred during the update exception will be thrown. * * @param \Magento\Catalog\Api\Data\TierPriceInterface[] $prices - * @return bool Will returned True if replaced. + * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] */ public function replace(array $prices); /** * Delete product tier prices. + * If any items will have invalid price, price type, website id, sku, customer group or quantity, they will be + * marked as failed and excluded from delete list and \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] + * with problem description will be returned. + * If there were no failed items during update empty array will be returned. + * If error occurred during the update exception will be thrown. * * @param \Magento\Catalog\Api\Data\TierPriceInterface[] $prices - * @return bool Will returned True if deleted. + * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] */ public function delete(array $prices); } diff --git a/app/code/Magento/Catalog/Model/Product/Price/BasePriceStorage.php b/app/code/Magento/Catalog/Model/Product/Price/BasePriceStorage.php index 50cffd047ccef148654c27fa938f2dc7879c1785..b9ffb332e1cdd93e8c1597880e9a90b66281b2ee 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/BasePriceStorage.php +++ b/app/code/Magento/Catalog/Model/Product/Price/BasePriceStorage.php @@ -43,6 +43,21 @@ class BasePriceStorage implements \Magento\Catalog\Api\BasePriceStorageInterface */ private $productRepository; + /** + * @var \Magento\Catalog\Model\Product\Price\Validation\Result + */ + private $validationResult; + + /** + * @var PricePersistenceFactory + */ + private $pricePersistenceFactory; + + /** + * @var \Magento\Catalog\Model\Product\Price\InvalidSkuChecker + */ + private $invalidSkuChecker; + /** * Price type allowed. * @@ -58,19 +73,14 @@ class BasePriceStorage implements \Magento\Catalog\Api\BasePriceStorageInterface 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 + * @param \Magento\Catalog\Model\Product\Price\Validation\Result $validationResult + * @param \Magento\Catalog\Model\Product\Price\InvalidSkuChecker $invalidSkuChecker + * @param array $allowedProductTypes [optional] */ public function __construct( PricePersistenceFactory $pricePersistenceFactory, @@ -78,6 +88,8 @@ class BasePriceStorage implements \Magento\Catalog\Api\BasePriceStorageInterface \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator, \Magento\Store\Api\StoreRepositoryInterface $storeRepository, \Magento\Catalog\Api\ProductRepositoryInterface $productRepository, + \Magento\Catalog\Model\Product\Price\Validation\Result $validationResult, + \Magento\Catalog\Model\Product\Price\InvalidSkuChecker $invalidSkuChecker, array $allowedProductTypes = [] ) { $this->pricePersistenceFactory = $pricePersistenceFactory; @@ -85,7 +97,9 @@ class BasePriceStorage implements \Magento\Catalog\Api\BasePriceStorageInterface $this->productIdLocator = $productIdLocator; $this->storeRepository = $storeRepository; $this->productRepository = $productRepository; + $this->validationResult = $validationResult; $this->allowedProductTypes = $allowedProductTypes; + $this->invalidSkuChecker = $invalidSkuChecker; } /** @@ -93,7 +107,11 @@ class BasePriceStorage implements \Magento\Catalog\Api\BasePriceStorageInterface */ public function get(array $skus) { - $this->validateSkus($skus); + $this->invalidSkuChecker->isSkuListValid( + $skus, + $this->allowedProductTypes, + $this->priceTypeAllowed + ); $rawPrices = $this->getPricePersistence()->get($skus); $prices = []; foreach ($rawPrices as $rawPrice) { @@ -114,7 +132,7 @@ class BasePriceStorage implements \Magento\Catalog\Api\BasePriceStorageInterface */ public function update(array $prices) { - $this->validate($prices); + $prices = $this->retrieveValidPrices($prices); $formattedPrices = []; foreach ($prices as $price) { @@ -130,7 +148,7 @@ class BasePriceStorage implements \Magento\Catalog\Api\BasePriceStorageInterface $this->getPricePersistence()->update($formattedPrices); - return true; + return $this->validationResult->getFailedItems(); } /** @@ -148,80 +166,59 @@ class BasePriceStorage implements \Magento\Catalog\Api\BasePriceStorageInterface } /** - * Validate SKU, check product types and skip not existing products. + * Retrieve valid prices that do not contain any errors. * - * @param array $skus - * @throws \Magento\Framework\Exception\LocalizedException - * @return void + * @param \Magento\Catalog\Api\Data\BasePriceInterface[] $prices + * @return array */ - 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) + private function retrieveValidPrices(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); + $invalidSkus = $this->invalidSkuChecker->retrieveInvalidSkuList( + $skus, + $this->allowedProductTypes, + $this->priceTypeAllowed + ); - foreach ($prices as $price) { + foreach ($prices as $id => $price) { + if (!$price->getSku() || in_array($price->getSku(), $invalidSkus)) { + $this->validationResult->addFailedItem( + $id, + __( + 'Invalid attribute %fieldName = %fieldValue.', + ['fieldName' => '%fieldName', 'fieldValue' => '%fieldValue'] + ), + ['fieldName' => 'SKU', 'fieldValue' => $price->getSku()] + ); + } if (null === $price->getPrice() || $price->getPrice() < 0) { - throw new \Magento\Framework\Exception\LocalizedException( + $this->validationResult->addFailedItem( + $id, __( - 'Invalid attribute %fieldName: %fieldValue.', - [ - 'fieldName' => 'Price', - 'fieldValue' => $price->getPrice() - ] - ) + 'Invalid attribute %fieldName = %fieldValue.', + ['fieldName' => '%fieldName', 'fieldValue' => '%fieldValue'] + ), + ['fieldName' => 'Price', 'fieldValue' => $price->getPrice()] + ); + } + try { + $this->storeRepository->getById($price->getStoreId()); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + $this->validationResult->addFailedItem( + $id, + __('Requested store is not found.') ); } - $this->storeRepository->getById($price->getStoreId()); } + + foreach ($this->validationResult->getFailedRowIds() as $id) { + unset($prices[$id]); + } + + return $prices; } } diff --git a/app/code/Magento/Catalog/Model/Product/Price/CostStorage.php b/app/code/Magento/Catalog/Model/Product/Price/CostStorage.php index ce1fa2e0e6efbc75bca3061e0a0539c5f2559615..d5f9d08e9115ddb72351cde957df3b8dce7bee57 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/CostStorage.php +++ b/app/code/Magento/Catalog/Model/Product/Price/CostStorage.php @@ -33,6 +33,16 @@ class CostStorage implements \Magento\Catalog\Api\CostStorageInterface */ private $productIdLocator; + /** + * @var \Magento\Catalog\Model\Product\Price\Validation\Result + */ + private $validationResult; + + /** + * @var \Magento\Catalog\Model\Product\Price\InvalidSkuChecker + */ + private $invalidSkuChecker; + /** * Allowed product types. * @@ -57,19 +67,25 @@ class CostStorage implements \Magento\Catalog\Api\CostStorageInterface * @param \Magento\Catalog\Api\Data\CostInterfaceFactory $costInterfaceFactory * @param \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator * @param \Magento\Store\Api\StoreRepositoryInterface $storeRepository - * @param array $allowedProductTypes + * @param \Magento\Catalog\Model\Product\Price\Validation\Result $validationResult + * @param \Magento\Catalog\Model\Product\Price\InvalidSkuChecker $invalidSkuChecker + * @param array $allowedProductTypes [optional] */ public function __construct( PricePersistenceFactory $pricePersistenceFactory, \Magento\Catalog\Api\Data\CostInterfaceFactory $costInterfaceFactory, \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator, \Magento\Store\Api\StoreRepositoryInterface $storeRepository, + \Magento\Catalog\Model\Product\Price\Validation\Result $validationResult, + \Magento\Catalog\Model\Product\Price\InvalidSkuChecker $invalidSkuChecker, array $allowedProductTypes = [] ) { $this->pricePersistenceFactory = $pricePersistenceFactory; $this->costInterfaceFactory = $costInterfaceFactory; $this->productIdLocator = $productIdLocator; $this->storeRepository = $storeRepository; + $this->validationResult = $validationResult; + $this->invalidSkuChecker = $invalidSkuChecker; $this->allowedProductTypes = $allowedProductTypes; } @@ -78,7 +94,7 @@ class CostStorage implements \Magento\Catalog\Api\CostStorageInterface */ public function get(array $skus) { - $this->validateSkus($skus); + $this->invalidSkuChecker->isSkuListValid($skus, $this->allowedProductTypes); $rawPrices = $this->getPricePersistence()->get($skus); $prices = []; foreach ($rawPrices as $rawPrice) { @@ -99,12 +115,13 @@ class CostStorage implements \Magento\Catalog\Api\CostStorageInterface */ public function update(array $prices) { - $this->validate($prices); + $prices = $this->retrieveValidPrices($prices); $formattedPrices = []; foreach ($prices as $price) { - $ids = array_keys($this->productIdLocator->retrieveProductIdsBySkus([$price->getSku()])[$price->getSku()]); - foreach ($ids as $id) { + $productIdsBySkus = $this->productIdLocator->retrieveProductIdsBySkus([$price->getSku()]); + $productIds = array_keys($productIdsBySkus[$price->getSku()]); + foreach ($productIds as $id) { $formattedPrices[] = [ 'store_id' => $price->getStoreId(), $this->getPricePersistence()->getEntityLinkField() => $id, @@ -115,7 +132,7 @@ class CostStorage implements \Magento\Catalog\Api\CostStorageInterface $this->getPricePersistence()->update($formattedPrices); - return true; + return $this->validationResult->getFailedItems(); } /** @@ -123,7 +140,7 @@ class CostStorage implements \Magento\Catalog\Api\CostStorageInterface */ public function delete(array $skus) { - $this->validateSkus($skus); + $this->invalidSkuChecker->isSkuListValid($skus, $this->allowedProductTypes); $this->getPricePersistence()->delete($skus); return true; @@ -144,75 +161,55 @@ class CostStorage implements \Magento\Catalog\Api\CostStorageInterface } /** - * Validate that prices have appropriate values. + * Retrieve valid prices that do not contain any errors. * * @param array $prices - * @throws \Magento\Framework\Exception\LocalizedException - * @return void + * @return array */ - private function validate(array $prices) + private function retrieveValidPrices(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); + $invalidSkus = $this->invalidSkuChecker->retrieveInvalidSkuList($skus, $this->allowedProductTypes); - foreach ($prices as $price) { + foreach ($prices as $id => $price) { + if (!$price->getSku() || in_array($price->getSku(), $invalidSkus)) { + $this->validationResult->addFailedItem( + $id, + __( + 'Invalid attribute %fieldName = %fieldValue.', + ['fieldName' => '%fieldName', 'fieldValue' => '%fieldValue'] + ), + ['fieldName' => 'SKU', 'fieldValue' => $price->getSku()] + ); + } if (null === $price->getCost() || $price->getCost() < 0) { - throw new \Magento\Framework\Exception\LocalizedException( + $this->validationResult->addFailedItem( + $id, __( - 'Invalid attribute %fieldName: %fieldValue.', - [ - 'fieldName' => 'Cost', - 'fieldValue' => $price->getCost() - ] - ) + 'Invalid attribute %fieldName = %fieldValue.', + ['fieldName' => '%fieldName', 'fieldValue' => '%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; - } + try { + $this->storeRepository->getById($price->getStoreId()); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + $this->validationResult->addFailedItem( + $id, + __('Requested store is not found.') + ); } } - 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); + foreach ($this->validationResult->getFailedRowIds() as $id) { + unset($prices[$id]); } + + return $prices; } } diff --git a/app/code/Magento/Catalog/Model/Product/Price/InvalidSkuChecker.php b/app/code/Magento/Catalog/Model/Product/Price/InvalidSkuChecker.php new file mode 100644 index 0000000000000000000000000000000000000000..2b8fa682b48facfac7961d818aee209c957cf00b --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Price/InvalidSkuChecker.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\Product\Price; + +/** + * Class is responsible to detect list of invalid SKU values from list of provided skus and allowed product types. + */ +class InvalidSkuChecker +{ + /** + * @param \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator + * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + */ + public function __construct( + \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator, + \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + ) { + $this->productIdLocator = $productIdLocator; + $this->productRepository = $productRepository; + } + + /** + * Retrieve not found or invalid SKUs and and check that their type corresponds to allowed types list. + * + * @param array $skus + * @param array $allowedProductTypes + * @param int|bool $allowedPriceTypeValue + * @return array + */ + public function retrieveInvalidSkuList(array $skus, array $allowedProductTypes, $allowedPriceTypeValue = false) + { + $idsBySku = $this->productIdLocator->retrieveProductIdsBySkus($skus); + $skuDiff = array_diff($skus, array_keys($idsBySku)); + + foreach ($idsBySku as $sku => $ids) { + foreach ($ids as $type) { + $valueTypeIsAllowed = false; + + if ($allowedPriceTypeValue + && $type == \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE + && $this->productRepository->get($sku)->getPriceType() != $allowedProductTypes + ) { + $valueTypeIsAllowed = true; + } + + if (!in_array($type, $allowedProductTypes) || $valueTypeIsAllowed) { + $skuDiff[] = $sku; + break; + } + } + } + + return $skuDiff; + } + + /** + * Check that SKU list is valid or return exception if it contains invalid values. + * + * @param array $skus + * @param array $allowedProductTypes + * @param int|bool $allowedPriceTypeValue + * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function isSkuListValid(array $skus, array $allowedProductTypes, $allowedPriceTypeValue = false) + { + $failedItems = $this->retrieveInvalidSkuList($skus, $allowedProductTypes, $allowedPriceTypeValue); + + if (!empty($failedItems)) { + $values = implode(', ', $failedItems); + $description = count($failedItems) == 1 + ? __('Requested product doesn\'t exist: %sku', ['sku' => $values]) + : __('Requested products don\'t exist: %sku', ['sku' => $values]); + throw new \Magento\Framework\Exception\NoSuchEntityException($description); + } + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Price/PriceUpdateResult.php b/app/code/Magento/Catalog/Model/Product/Price/PriceUpdateResult.php new file mode 100644 index 0000000000000000000000000000000000000000..c94f30512176471ad94defb63137173a4cdf5414 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Price/PriceUpdateResult.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\Product\Price; + +use Magento\Catalog\Api\Data\PriceUpdateResultInterface; + +/** + * {@inheritdoc} + */ +class PriceUpdateResult extends \Magento\Framework\Model\AbstractExtensibleModel implements PriceUpdateResultInterface +{ + /** + * {@inheritdoc} + */ + public function getMessage() + { + return $this->getData(self::MESSAGE); + } + + /** + * {@inheritdoc} + */ + public function setMessage($message) + { + return $this->setData(self::MESSAGE, $message); + } + + /** + * {@inheritdoc} + */ + public function getParameters() + { + return $this->getData(self::PARAMETERS); + } + + /** + * {@inheritdoc} + */ + public function setParameters(array $parameters) + { + return $this->setData(self::PARAMETERS, $parameters); + } + + /** + * {@inheritdoc} + */ + public function getExtensionAttributes() + { + return $this->_getExtensionAttributes(); + } + + /** + * {@inheritdoc} + */ + public function setExtensionAttributes( + \Magento\Catalog\Api\Data\PriceUpdateResultExtensionInterface $extensionAttributes + ) { + return $this->_setExtensionAttributes($extensionAttributes); + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Price/SpecialPrice.php b/app/code/Magento/Catalog/Model/Product/Price/SpecialPrice.php new file mode 100644 index 0000000000000000000000000000000000000000..591252dcf441d39dbf7da492cb7f231354b77219 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Price/SpecialPrice.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\Product\Price; + +use Magento\Catalog\Api\Data\SpecialPriceInterface; + +/** + * Product Special Price class is used to encapsulate data that can be processed by efficient price API. + */ +class SpecialPrice extends \Magento\Framework\Model\AbstractExtensibleModel implements SpecialPriceInterface +{ + /** + * {@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 setPriceFrom($datetime) + { + return $this->setData(self::PRICE_FROM, $datetime); + } + + /** + * {@inheritdoc} + */ + public function getPriceFrom() + { + return $this->getData(self::PRICE_FROM); + } + + /** + * {@inheritdoc} + */ + public function setPriceTo($datetime) + { + return $this->setData(self::PRICE_TO, $datetime); + } + + /** + * {@inheritdoc} + */ + public function getPriceTo() + { + return $this->getData(self::PRICE_TO); + } + + /** + * {@inheritdoc} + */ + public function getExtensionAttributes() + { + return $this->_getExtensionAttributes(); + } + + /** + * {@inheritdoc} + */ + public function setExtensionAttributes( + \Magento\Catalog\Api\Data\SpecialPriceExtensionInterface $extensionAttributes + ) { + return $this->_setExtensionAttributes($extensionAttributes); + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Price/SpecialPriceStorage.php b/app/code/Magento/Catalog/Model/Product/Price/SpecialPriceStorage.php new file mode 100644 index 0000000000000000000000000000000000000000..dba621d780883271c345f18a5f52168630ed6ff9 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Price/SpecialPriceStorage.php @@ -0,0 +1,245 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\Product\Price; + +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Special price storage presents efficient price API and is used to retrieve, update or delete special prices. + */ +class SpecialPriceStorage implements \Magento\Catalog\Api\SpecialPriceStorageInterface +{ + /** + * @var \Magento\Catalog\Api\SpecialPriceInterface + */ + private $specialPriceResource; + + /** + * @var \Magento\Catalog\Api\Data\SpecialPriceInterfaceFactory + */ + private $specialPriceFactory; + + /** + * @var \Magento\Catalog\Model\ProductIdLocatorInterface + */ + private $productIdLocator; + + /** + * @var \Magento\Store\Api\StoreRepositoryInterface + */ + private $storeRepository; + + /** + * @var \Magento\Catalog\Model\Product\Price\Validation\Result + */ + private $validationResult; + + /** + * @var \Magento\Catalog\Model\Product\Price\InvalidSkuChecker + */ + private $invalidSkuChecker; + + /** + * @var array + */ + private $allowedProductTypes = []; + + /** + * @param \Magento\Catalog\Api\SpecialPriceInterface $specialPriceResource + * @param \Magento\Catalog\Api\Data\SpecialPriceInterfaceFactory $specialPriceFactory + * @param \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator + * @param \Magento\Store\Api\StoreRepositoryInterface $storeRepository + * @param \Magento\Catalog\Model\Product\Price\Validation\Result $validationResult + * @param \Magento\Catalog\Model\Product\Price\InvalidSkuChecker $invalidSkuChecker + * @param array $allowedProductTypes [optional] + */ + public function __construct( + \Magento\Catalog\Api\SpecialPriceInterface $specialPriceResource, + \Magento\Catalog\Api\Data\SpecialPriceInterfaceFactory $specialPriceFactory, + \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator, + \Magento\Store\Api\StoreRepositoryInterface $storeRepository, + \Magento\Catalog\Model\Product\Price\Validation\Result $validationResult, + \Magento\Catalog\Model\Product\Price\InvalidSkuChecker $invalidSkuChecker, + array $allowedProductTypes = [] + ) { + $this->specialPriceResource = $specialPriceResource; + $this->specialPriceFactory = $specialPriceFactory; + $this->productIdLocator = $productIdLocator; + $this->storeRepository = $storeRepository; + $this->validationResult = $validationResult; + $this->invalidSkuChecker = $invalidSkuChecker; + $this->allowedProductTypes = $allowedProductTypes; + } + + /** + * {@inheritdoc} + */ + public function get(array $skus) + { + $this->invalidSkuChecker->isSkuListValid($skus, $this->allowedProductTypes); + $rawPrices = $this->specialPriceResource->get($skus); + + $prices = []; + foreach ($rawPrices as $rawPrice) { + /** @var \Magento\Catalog\Api\Data\SpecialPriceInterface $price */ + $price = $this->specialPriceFactory->create(); + $sku = isset($rawPrice['sku']) + ? $rawPrice['sku'] + : $this->retrieveSkuById($rawPrice[$this->specialPriceResource->getEntityLinkField()], $skus); + $price->setSku($sku); + $price->setPrice($rawPrice['value']); + $price->setStoreId($rawPrice['store_id']); + $price->setPriceFrom($rawPrice['price_from']); + $price->setPriceTo($rawPrice['price_to']); + $prices[] = $price; + } + + return $prices; + } + + /** + * {@inheritdoc} + */ + public function update(array $prices) + { + $prices = $this->retrieveValidPrices($prices); + $this->specialPriceResource->update($prices); + + return $this->validationResult->getFailedItems(); + } + + /** + * {@inheritdoc} + */ + public function delete(array $prices) + { + $prices = $this->retrieveValidPrices($prices); + $this->specialPriceResource->delete($prices); + + return $this->validationResult->getFailedItems(); + } + + /** + * Retrieve prices with correct values. + * + * @param array $prices + * @return array + */ + private function retrieveValidPrices(array $prices) + { + $skus = array_unique( + array_map(function ($price) { + return $price->getSku(); + }, $prices) + ); + $failedSkus = $this->invalidSkuChecker->retrieveInvalidSkuList($skus, $this->allowedProductTypes); + + foreach ($prices as $key => $price) { + if (!$price->getSku() || in_array($price->getSku(), $failedSkus)) { + $this->validationResult->addFailedItem( + $key, + __( + 'Requested product doesn\'t exist: %sku', + ['sku' => '%sku'] + ), + ['sku' => $price->getSku()] + ); + } + $this->checkPrice($price->getPrice(), $key); + $this->checkDate($price->getPriceFrom(), 'Price From', $key); + $this->checkDate($price->getPriceTo(), 'Price To', $key); + try { + $this->storeRepository->getById($price->getStoreId()); + } catch (NoSuchEntityException $e) { + $this->validationResult->addFailedItem( + $key, + __('Requested store is not found.') + ); + } + } + + foreach ($this->validationResult->getFailedRowIds() as $id) { + unset($prices[$id]); + } + + return $prices; + } + + /** + * Check that date value is correct and add error to aggregator if it contains incorrect data. + * + * @param string $value + * @param string $label + * @param int $key + * @return void + */ + private function checkDate($value, $label, $key) + { + if ($value && !$this->isCorrectDateValue($value)) { + $this->validationResult->addFailedItem( + $key, + __( + 'Invalid attribute %fieldName = %fieldValue.', + ['fieldName' => '%fieldName', 'fieldValue' => '%fieldValue'] + ), + ['fieldName' => $label, 'fieldValue' => $value] + ); + } + } + + /** + * Check that provided price value is not empty and not lower then zero and add error to aggregator if price + * contains not valid data. + * + * @param float $price + * @param int $key + * @return void + */ + private function checkPrice($price, $key) + { + if (null === $price || $price < 0) { + $this->validationResult->addFailedItem( + $key, + __( + 'Invalid attribute %fieldName = %fieldValue.', + ['fieldName' => '%fieldName', 'fieldValue' => '%fieldValue'] + ), + ['fieldName' => 'Price', 'fieldValue' => $price] + ); + } + } + + /** + * Retrieve SKU by product ID. + * + * @param int $id + * @param array $skus + * @return int|null + */ + private function retrieveSkuById($id, array $skus) + { + foreach ($this->productIdLocator->retrieveProductIdsBySkus($skus) as $sku => $ids) { + if (false !== array_key_exists($id, $ids)) { + return $sku; + } + } + + return null; + } + + /** + * Check that date value is correct. + * + * @param string $date + * @return bool + */ + private function isCorrectDateValue($date) + { + $actualDate = date('Y-m-d H:i:s', strtotime($date)); + return $actualDate && $actualDate === $date; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php index 58d3804c2dac6bd82d01c2c4842fcdcbeb642494..b8e78380bbc31bcb1f4c1cc525146f6e4314adef 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php +++ b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php @@ -23,7 +23,7 @@ class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface /** * Tier price validator. * - * @var \Magento\Catalog\Model\Product\Price\TierPriceValidator + * @var \Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator */ private $tierPriceValidator; @@ -70,10 +70,8 @@ class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface private $indexerChunkValue = 500; /** - * TierPriceStorage constructor. - * * @param TierPricePersistence $tierPricePersistence - * @param TierPriceValidator $tierPriceValidator + * @param \Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator $tierPriceValidator * @param TierPriceFactory $tierPriceFactory * @param \Magento\Catalog\Model\Indexer\Product\Price $priceIndexer * @param \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator @@ -82,7 +80,7 @@ class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface */ public function __construct( TierPricePersistence $tierPricePersistence, - TierPriceValidator $tierPriceValidator, + \Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator $tierPriceValidator, TierPriceFactory $tierPriceFactory, \Magento\Catalog\Model\Indexer\Product\Price $priceIndexer, \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator, @@ -104,16 +102,8 @@ class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface 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; + return $this->getExistingPrices($skus); } /** @@ -127,13 +117,14 @@ class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface return $price->getSku(); }, $prices) ); - $this->tierPriceValidator->validatePrices($prices, $this->get($skus)); + $result = $this->tierPriceValidator->retrieveValidationResult($prices, $this->getExistingPrices($skus)); + $prices = $this->removeIncorrectPrices($prices, $result->getFailedRowIds()); $formattedPrices = $this->retrieveFormattedPrices($prices); $this->tierPricePersistence->update($formattedPrices); $this->reindexPrices($affectedIds); $this->invalidateFullPageCache(); - return true; + return $result->getFailedItems(); } /** @@ -141,14 +132,15 @@ class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface */ public function replace(array $prices) { - $this->tierPriceValidator->validatePrices($prices); + $result = $this->tierPriceValidator->retrieveValidationResult($prices); + $prices = $this->removeIncorrectPrices($prices, $result->getFailedRowIds()); $affectedIds = $this->retrieveAffectedProductIdsForPrices($prices); $formattedPrices = $this->retrieveFormattedPrices($prices); $this->tierPricePersistence->replace($formattedPrices, $affectedIds); $this->reindexPrices($affectedIds); $this->invalidateFullPageCache(); - return true; + return $result->getFailedItems(); } /** @@ -157,13 +149,34 @@ class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface public function delete(array $prices) { $affectedIds = $this->retrieveAffectedProductIdsForPrices($prices); - $this->tierPriceValidator->validatePrices($prices); + $result = $this->tierPriceValidator->retrieveValidationResult($prices); + $prices = $this->removeIncorrectPrices($prices, $result->getFailedRowIds()); $priceIds = $this->retrieveAffectedPriceIds($prices); $this->tierPricePersistence->delete($priceIds); $this->reindexPrices($affectedIds); $this->invalidateFullPageCache(); - return true; + return $result->getFailedItems(); + } + + /** + * Get existing prices by SKUs. + * + * @param array $skus + * @return array + */ + private function getExistingPrices(array $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; } /** @@ -242,11 +255,11 @@ class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface } /** - * Retrieve price ID. + * Look through provided price in list of existing prices and retrieve it's Id. * * @param array $price * @param array $existingPrices - * @return int + * @return int|void */ private function retrievePriceId(array $price, array $existingPrices) { @@ -265,7 +278,7 @@ class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface } /** - * Check is correct price value + * Check that price value or price percentage value is not equal to 0 and is not similar with existing value. * * @param array $existingPrice * @param array $price @@ -320,4 +333,20 @@ class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface $this->priceIndexer->execute($affectedIds); } } + + /** + * Remove prices from price list by id list. + * + * @param array $prices + * @param array $ids + * @return array + */ + private function removeIncorrectPrices(array $prices, array $ids) + { + foreach ($ids as $id) { + unset($prices[$id]); + } + + return $prices; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPriceValidator.php b/app/code/Magento/Catalog/Model/Product/Price/TierPriceValidator.php deleted file mode 100644 index 8a307d04fbfbecc705193832edd9b539467be62a..0000000000000000000000000000000000000000 --- a/app/code/Magento/Catalog/Model/Product/Price/TierPriceValidator.php +++ /dev/null @@ -1,351 +0,0 @@ -<?php -/** - * Copyright © 2013-2017 Magento, Inc. 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/Price/Validation/Result.php b/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php new file mode 100644 index 0000000000000000000000000000000000000000..3ec7ee7abe3a78f474fc32529ba2528c73098a33 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\Product\Price\Validation; + +/** + * Validation Result is used to aggregate errors that occurred during price update. + */ +class Result +{ + /** + * @var \Magento\Catalog\Api\Data\PriceUpdateResultInterfaceFactory + */ + private $priceUpdateResultFactory; + + /** + * Failed items. + * + * @var array + */ + private $failedItems = []; + + /** + * @param \Magento\Catalog\Api\Data\PriceUpdateResultInterfaceFactory $priceUpdateResultFactory + */ + public function __construct( + \Magento\Catalog\Api\Data\PriceUpdateResultInterfaceFactory $priceUpdateResultFactory + ) { + $this->priceUpdateResultFactory = $priceUpdateResultFactory; + } + + /** + * Add failed price identified, message and message parameters, that occurred during price update. + * + * @param int $id Failed price identified. + * @param string $message Failure reason message. + * @param array $parameters (optional). Placeholder values in ['placeholder key' => 'placeholder value'] format + * for failure reason message. + * @return void + */ + public function addFailedItem($id, $message, array $parameters = []) + { + $this->failedItems[$id][] = [ + 'message' => $message, + 'parameters' => $parameters + ]; + } + + /** + * Get ids of rows, that contained errors during price update. + * + * @return int[] + */ + public function getFailedRowIds() + { + return $this->failedItems ? array_keys($this->failedItems) : []; + } + + /** + * Get price update errors, that occurred during price update. + * + * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] + */ + public function getFailedItems() + { + $failedItems = []; + + foreach ($this->failedItems as $items) { + foreach ($items as $failedRecord) { + $resultItem = $this->priceUpdateResultFactory->create(); + $resultItem->setMessage($failedRecord['message']); + $resultItem->setParameters($failedRecord['parameters']); + $failedItems[] = $resultItem; + } + } + + return $failedItems; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Price/Validation/TierPriceValidator.php b/app/code/Magento/Catalog/Model/Product/Price/Validation/TierPriceValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..76da4b7011bd521f06c497856c11ee465f32d297 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Price/Validation/TierPriceValidator.php @@ -0,0 +1,414 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\Product\Price\Validation; + +/** + * Tier Price Validator. + */ +class TierPriceValidator +{ + /** + * @var \Magento\Catalog\Model\ProductIdLocatorInterface + */ + private $productIdLocator; + + /** + * @var \Magento\Framework\Api\SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var \Magento\Framework\Api\FilterBuilder + */ + private $filterBuilder; + + /** + * @var \Magento\Customer\Api\GroupRepositoryInterface + */ + private $customerGroupRepository; + + /** + * @var \Magento\Store\Api\WebsiteRepositoryInterface + */ + private $websiteRepository; + + /** + * @var \Magento\Catalog\Model\Product\Price\Validation\Result + */ + private $validationResult; + + /** + * @var \Magento\Catalog\Model\Product\Price\TierPricePersistence + */ + private $tierPricePersistence; + + /** + * Groups by code cache. + * + * @var array + */ + private $customerGroupsByCode = []; + + /** + * @var \Magento\Catalog\Model\Product\Price\InvalidSkuChecker + */ + private $invalidSkuChecker; + + /** + * 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 \Magento\Catalog\Model\Product\Price\TierPricePersistence $tierPricePersistence + * @param \Magento\Catalog\Model\Product\Price\Validation\Result $validationResult + * @param \Magento\Catalog\Model\Product\Price\InvalidSkuChecker $invalidSkuChecker + * @param array $allowedProductTypes [optional] + */ + 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, + \Magento\Catalog\Model\Product\Price\TierPricePersistence $tierPricePersistence, + \Magento\Catalog\Model\Product\Price\Validation\Result $validationResult, + \Magento\Catalog\Model\Product\Price\InvalidSkuChecker $invalidSkuChecker, + array $allowedProductTypes = [] + ) { + $this->productIdLocator = $productIdLocator; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->filterBuilder = $filterBuilder; + $this->customerGroupRepository = $customerGroupRepository; + $this->websiteRepository = $websiteRepository; + $this->tierPricePersistence = $tierPricePersistence; + $this->validationResult = $validationResult; + $this->invalidSkuChecker = $invalidSkuChecker; + $this->allowedProductTypes = $allowedProductTypes; + } + + /** + * Validate SKU. + * + * @param array $skus + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @return void + */ + public function validateSkus(array $skus) + { + $this->invalidSkuChecker->isSkuListValid($skus, $this->allowedProductTypes); + } + + /** + * Validate that prices have appropriate values and are unique and return result. + * + * @param array $prices + * @param array $existingPrices + * @return \Magento\Catalog\Model\Product\Price\Validation\Result $validationResult + */ + public function retrieveValidationResult(array $prices, array $existingPrices = []) + { + $validationResult = clone $this->validationResult; + $skus = array_unique( + array_map(function ($price) { + return $price->getSku(); + }, $prices) + ); + $skuDiff = $this->invalidSkuChecker->retrieveInvalidSkuList($skus, $this->allowedProductTypes); + $idsBySku = $this->productIdLocator->retrieveProductIdsBySkus($skus); + + $pricesBySku = []; + + foreach ($prices as $price) { + $pricesBySku[$price->getSku()][] = $price; + } + + foreach ($prices as $key => $price) { + $this->checkSku($price, $key, $skuDiff, $validationResult); + $this->checkPrice($price, $key, $validationResult); + $ids = isset($idsBySku[$price->getSku()]) ? $idsBySku[$price->getSku()] : []; + $this->checkPriceType($price, $ids, $key, $validationResult); + $this->checkQuantity($price, $key, $validationResult); + $this->checkWebsite($price, $key, $validationResult); + if (isset($pricesBySku[$price->getSku()])) { + $this->checkUnique($price, $pricesBySku[$price->getSku()], $key, $validationResult); + } + $this->checkUnique($price, $existingPrices, $key, $validationResult); + $this->checkGroup($price, $key, $validationResult); + } + + return $validationResult; + } + + /** + * Check that sku value is correct. + * + * @param \Magento\Catalog\Api\Data\TierPriceInterface $price + * @param int $key + * @param array $invalidSkus + * @param Result $validationResult + * @return void + */ + private function checkSku( + \Magento\Catalog\Api\Data\TierPriceInterface $price, + $key, + array $invalidSkus, + Result $validationResult + ) { + if (!$price->getSku() || in_array($price->getSku(), $invalidSkus)) { + $validationResult->addFailedItem( + $key, + __( + 'Invalid attribute %fieldName = %fieldValue.', + ['fieldName' => '%fieldName', 'fieldValue' => '%fieldValue'] + ), + ['fieldName' => 'SKU', 'fieldValue' => $price->getSku()] + ); + } + } + + /** + * Verify that price value is correct. + * + * @param \Magento\Catalog\Api\Data\TierPriceInterface $price + * @param int $key + * @param Result $validationResult + * @return void + */ + private function checkPrice(\Magento\Catalog\Api\Data\TierPriceInterface $price, $key, Result $validationResult) + { + if ( + null === $price->getPrice() + || $price->getPrice() < 0 + || ($price->getPriceType() === \Magento\Catalog\Api\Data\TierPriceInterface::PRICE_TYPE_DISCOUNT + && $price->getPrice() > 100 + ) + ) { + $validationResult->addFailedItem( + $key, + __( + 'Invalid attribute %fieldName = %fieldValue.', + ['fieldName' => '%fieldName', 'fieldValue' => '%fieldValue'] + ), + ['fieldName' => 'Price', 'fieldValue' => $price->getPrice()] + ); + } + } + + /** + * Verify that price type is correct. + * + * @param \Magento\Catalog\Api\Data\TierPriceInterface $price + * @param array $ids + * @param int $key + * @param Result $validationResult + * @return void + */ + private function checkPriceType( + \Magento\Catalog\Api\Data\TierPriceInterface $price, + array $ids, + $key, + Result $validationResult + ) { + if ( + !in_array( + $price->getPriceType(), + [ + \Magento\Catalog\Api\Data\TierPriceInterface::PRICE_TYPE_FIXED, + \Magento\Catalog\Api\Data\TierPriceInterface::PRICE_TYPE_DISCOUNT + ] + ) + || (array_search(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE, $ids) + && $price->getPriceType() !== \Magento\Catalog\Api\Data\TierPriceInterface::PRICE_TYPE_DISCOUNT) + ) { + $validationResult->addFailedItem( + $key, + __( + 'Invalid attribute %fieldName = %fieldValue.', + ['fieldName' => '%fieldName', 'fieldValue' => '%fieldValue'] + ), + ['fieldName' => 'Price Type', 'fieldValue' => $price->getPriceType()] + ); + } + } + + /** + * Verify that product quantity is correct. + * + * @param \Magento\Catalog\Api\Data\TierPriceInterface $price + * @param int $key + * @param Result $validationResult + * @return void + */ + private function checkQuantity(\Magento\Catalog\Api\Data\TierPriceInterface $price, $key, Result $validationResult) + { + if ($price->getQuantity() < 1) { + $validationResult->addFailedItem( + $key, + __( + 'Invalid attribute %fieldName = %fieldValue.', + ['fieldName' => '%fieldName', 'fieldValue' => '%fieldValue'] + ), + ['fieldName' => 'Quantity', 'fieldValue' => $price->getQuantity()] + ); + } + } + + /** + * Verify that website exists. + * + * @param \Magento\Catalog\Api\Data\TierPriceInterface $price + * @param int $key + * @param Result $validationResult + * @return void + */ + private function checkWebsite(\Magento\Catalog\Api\Data\TierPriceInterface $price, $key, Result $validationResult) + { + try { + $this->websiteRepository->getById($price->getWebsiteId()); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + $validationResult->addFailedItem( + $key, + __( + 'Invalid attribute %fieldName = %fieldValue.', + ['fieldName' => '%fieldName', 'fieldValue' => '%fieldValue'] + ), + ['fieldName' => 'Website Id', 'fieldValue' => $price->getWebsiteId()] + ); + } + } + + /** + * Check website value is unique. + * + * @param \Magento\Catalog\Api\Data\TierPriceInterface $tierPrice + * @param array $prices + * @param int $key + * @param Result $validationResult + * @return void + */ + private function checkUnique( + \Magento\Catalog\Api\Data\TierPriceInterface $tierPrice, + array $prices, + $key, + Result $validationResult + ) { + foreach ($prices as $price) { + if ( + $price->getSku() === $tierPrice->getSku() + && strtolower($price->getCustomerGroup()) === strtolower($tierPrice->getCustomerGroup()) + && $price->getQuantity() == $tierPrice->getQuantity() + && ( + ($price->getWebsiteId() == $this->allWebsitesValue + || $tierPrice->getWebsiteId() == $this->allWebsitesValue) + && $price->getWebsiteId() != $tierPrice->getWebsiteId() + ) + ) { + $validationResult->addFailedItem( + $key, + __( + 'We found a duplicate website, tier price, customer group and quantity:' + . ' %fieldName1 = %fieldValue1, %fieldName2 = %fieldValue2, %fieldName3 = %fieldValue3.', + [ + 'fieldName1' => '%fieldName1', + 'fieldValue1' => '%fieldValue1', + 'fieldName2' => '%fieldName2', + 'fieldValue2' => '%fieldValue2', + 'fieldName3' => '%fieldName3', + 'fieldValue3' => '%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 \Magento\Catalog\Api\Data\TierPriceInterface $price + * @param int $key + * @param Result $validationResult + * @return void + */ + private function checkGroup(\Magento\Catalog\Api\Data\TierPriceInterface $price, $key, Result $validationResult) + { + $customerGroup = strtolower($price->getCustomerGroup()); + + if ($customerGroup != $this->allGroupsValue && false === $this->retrieveGroupValue($customerGroup)) { + $validationResult->addFailedItem( + $key, + __( + 'No such entity with %fieldName = %fieldValue.', + ['fieldName' => '%fieldName', 'fieldValue' => '%fieldValue'] + ), + [ + 'fieldName' => 'Customer Group', + 'fieldValue' => $customerGroup, + ] + ); + } + } + + /** + * Retrieve customer group id by code. + * + * @param string $code + * @return int|bool + */ + 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) { + return false; + } + + $this->customerGroupsByCode[strtolower($item->getCode())] = $item->getId(); + } + + return $this->customerGroupsByCode[$code]; + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Price/SpecialPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Price/SpecialPrice.php new file mode 100644 index 0000000000000000000000000000000000000000..62ada0736ba8821049c9cd45dfccf3d7ba2fb01c --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Price/SpecialPrice.php @@ -0,0 +1,320 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\ResourceModel\Product\Price; + +/** + * Special price resource. + */ +class SpecialPrice implements \Magento\Catalog\Api\SpecialPriceInterface +{ + /** + * Price storage table. + * + * @var string + */ + private $priceTable = 'catalog_product_entity_decimal'; + + /** + * Datetime storage table. + * + * @var string + */ + private $datetimeTable = 'catalog_product_entity_datetime'; + + /** + * @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; + + /** + * Special Price attribute ID. + * + * @var int + */ + private $priceAttributeId; + + /** + * Special price from attribute ID. + * + * @var int + */ + private $priceFromAttributeId; + + /** + * Special price to attribute ID. + * + * @var int + */ + private $priceToAttributeId; + + /** + * Items per operation. + * + * @var int + */ + private $itemsPerOperation = 500; + + /** + * @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 + */ + public function __construct( + \Magento\Catalog\Model\ResourceModel\Attribute $attributeResource, + \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository, + \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator, + \Magento\Framework\EntityManager\MetadataPool $metadataPool + ) { + $this->attributeResource = $attributeResource; + $this->attributeRepository = $attributeRepository; + $this->productIdLocator = $productIdLocator; + $this->metadataPool = $metadataPool; + } + + /** + * {@inheritdoc} + */ + public function get(array $skus) + { + $ids = $this->retrieveAffectedIds($skus); + $priceTable = $this->attributeResource->getTable($this->priceTable); + $dateTimeTable = $this->attributeResource->getTable($this->datetimeTable); + $linkField = $this->getEntityLinkField(); + $select = $this->attributeResource->getConnection() + ->select() + ->from( + $priceTable, + [ + 'value_id', + 'store_id', + $this->getEntityLinkField(), + 'value', + ] + ) + ->joinLeft( + $dateTimeTable . ' AS datetime_from', + $priceTable . '.' . $linkField . '=' . 'datetime_from.' . $linkField + . ' AND datetime_from.attribute_id=' . $this->getPriceFromAttributeId(), + 'value AS price_from' + ) + ->joinLeft( + $dateTimeTable . ' AS datetime_to', + $priceTable . '.' . $linkField . '=' . 'datetime_to.' . $linkField + . ' AND datetime_to.attribute_id=' . $this->getPriceToAttributeId(), + 'value AS price_to' + ) + ->where($priceTable . '.' . $linkField . ' IN (?)', $ids) + ->where($priceTable . '.attribute_id = ?', $this->getPriceAttributeId()); + + return $this->attributeResource->getConnection()->fetchAll($select); + } + + /** + * {@inheritdoc} + */ + public function update(array $prices) + { + $formattedPrices = []; + $formattedDates = []; + + /** @var \Magento\Catalog\Api\Data\SpecialPriceInterface $price */ + foreach ($prices as $price) { + $productIdsBySku = $this->productIdLocator->retrieveProductIdsBySkus([$price->getSku()]); + $ids = array_keys($productIdsBySku[$price->getSku()]); + foreach ($ids as $id) { + $formattedPrices[] = [ + 'store_id' => $price->getStoreId(), + $this->getEntityLinkField() => $id, + 'value' => $price->getPrice(), + 'attribute_id' => $this->getPriceAttributeId(), + ]; + if ($price->getPriceFrom()) { + $formattedDates[] = [ + 'store_id' => $price->getStoreId(), + $this->getEntityLinkField() => $id, + 'value' => $price->getPriceFrom(), + 'attribute_id' => $this->getPriceFromAttributeId(), + ]; + } + if ($price->getPriceTo()) { + $formattedDates[] = [ + 'store_id' => $price->getStoreId(), + $this->getEntityLinkField() => $id, + 'value' => $price->getPriceTo(), + 'attribute_id' => $this->getPriceToAttributeId(), + ]; + } + } + } + $connection = $this->attributeResource->getConnection(); + $connection->beginTransaction(); + + try { + $this->updateItems($formattedPrices, $this->priceTable); + $this->updateItems($formattedDates, $this->datetimeTable); + $connection->commit(); + } catch (\Exception $e) { + $connection->rollBack(); + throw new \Magento\Framework\Exception\CouldNotSaveException( + __('Could not save Prices.'), + $e + ); + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function delete(array $prices) + { + $skus = array_unique( + array_map(function ($price) { + return $price->getSku(); + }, $prices) + ); + $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->priceTable), + [ + 'attribute_id = ?' => $this->getPriceAttributeId(), + $this->getEntityLinkField() . ' IN (?)' => $idsBunch + ] + ); + } + foreach (array_chunk($ids, $this->itemsPerOperation) as $idsBunch) { + $this->attributeResource->getConnection()->delete( + $this->attributeResource->getTable($this->datetimeTable), + [ + 'attribute_id IN (?)' => [$this->getPriceFromAttributeId(), $this->getPriceToAttributeId()], + $this->getEntityLinkField() . ' IN (?)' => $idsBunch + ] + ); + } + $connection->commit(); + } catch (\Exception $e) { + $connection->rollBack(); + throw new \Magento\Framework\Exception\CouldNotDeleteException( + __('Could not delete Prices'), + $e + ); + } + + return true; + } + + /** + * Get link field. + * + * @return string + */ + public function getEntityLinkField() + { + return $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) + ->getLinkField(); + } + + /** + * Update items in database. + * + * @param array $items + * @param string $table + * @return void + */ + private function updateItems(array $items, $table) + { + foreach (array_chunk($items, $this->itemsPerOperation) as $itemsBunch) { + $this->attributeResource->getConnection()->insertOnDuplicate( + $this->attributeResource->getTable($table), + $itemsBunch, + ['value'] + ); + } + } + + /** + * Get special price attribute ID. + * + * @return int + */ + private function getPriceAttributeId() + { + if (!$this->priceAttributeId) { + $this->priceAttributeId = $this->attributeRepository->get('special_price')->getAttributeId(); + } + + return $this->priceAttributeId; + } + + /** + * Get special price from attribute ID. + * + * @return int + */ + private function getPriceFromAttributeId() + { + if (!$this->priceFromAttributeId) { + $this->priceFromAttributeId = $this->attributeRepository->get('special_from_date')->getAttributeId(); + } + + return $this->priceFromAttributeId; + } + + /** + * Get special price to attribute ID. + * + * @return int + */ + private function getPriceToAttributeId() + { + if (!$this->priceToAttributeId) { + $this->priceToAttributeId = $this->attributeRepository->get('special_to_date')->getAttributeId(); + } + + return $this->priceToAttributeId; + } + + /** + * Retrieve IDs of products, that were affected during price update. + * + * @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); + } +} 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 index 61ea55228df9fc676a4634d61b72088b44ed855d..fe268803caba47816754a370f5d53b46d3d9f465 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/BasePriceStorageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/BasePriceStorageTest.php @@ -8,6 +8,8 @@ namespace Magento\Catalog\Test\Unit\Model\Product\Price; /** * Class BasePriceStorageTest. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class BasePriceStorageTest extends \PHPUnit_Framework_TestCase { @@ -51,6 +53,16 @@ class BasePriceStorageTest extends \PHPUnit_Framework_TestCase */ private $product; + /** + * @var \Magento\Catalog\Model\Product\Price\InvalidSkuChecker|\PHPUnit_Framework_MockObject_MockObject + */ + private $invalidSkuChecker; + + /** + * @var \Magento\Catalog\Model\Product\Price\Validation\Result|\PHPUnit_Framework_MockObject_MockObject + */ + private $validationResult; + /** * @var \Magento\Catalog\Model\Product\Price\BasePriceStorage */ @@ -60,6 +72,7 @@ class BasePriceStorageTest extends \PHPUnit_Framework_TestCase * Set up. * * @return void + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function setUp() { @@ -129,7 +142,24 @@ class BasePriceStorageTest extends \PHPUnit_Framework_TestCase true, ['getPriceType'] ); - + $this->invalidSkuChecker = $this->getMockForAbstractClass( + \Magento\Catalog\Model\Product\Price\InvalidSkuChecker::class, + [], + '', + false, + true, + true, + ['retrieveInvalidSkuList'] + ); + $this->validationResult = $this->getMockForAbstractClass( + \Magento\Catalog\Model\Product\Price\Validation\Result::class, + [], + '', + false, + true, + true, + ['getFailedRowIds', 'getFailedItems'] + ); $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->model = $objectManager->getObject( \Magento\Catalog\Model\Product\Price\BasePriceStorage::class, @@ -139,6 +169,8 @@ class BasePriceStorageTest extends \PHPUnit_Framework_TestCase 'productIdLocator' => $this->productIdLocator, 'storeRepository' => $this->storeRepository, 'productRepository' => $this->productRepository, + 'invalidSkuChecker' => $this->invalidSkuChecker, + 'validationResult' => $this->validationResult, 'allowedProductTypes' => ['simple', 'virtual', 'bundle', 'downloadable'], ] ); @@ -152,16 +184,6 @@ class BasePriceStorageTest extends \PHPUnit_Framework_TestCase 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, @@ -174,12 +196,6 @@ class BasePriceStorageTest extends \PHPUnit_Framework_TestCase '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') @@ -210,6 +226,7 @@ class BasePriceStorageTest extends \PHPUnit_Framework_TestCase ->method('setStoreId') ->withConsecutive([1], [1]) ->willReturnSelf(); + $this->invalidSkuChecker->expects($this->once())->method('retrieveInvalidSkuList')->willReturn([]); $this->model->get($skus); } @@ -222,20 +239,7 @@ class BasePriceStorageTest extends \PHPUnit_Framework_TestCase 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->invalidSkuChecker->expects($this->once())->method('retrieveInvalidSkuList')->willReturn($skus); $this->model->get($skus); } @@ -256,16 +260,18 @@ class BasePriceStorageTest extends \PHPUnit_Framework_TestCase $idsBySku = [ 'sku_1' => [ - 1 => \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE + 1 => [ + $this->basePriceInterface + ] ] ]; - $this->basePriceInterface->expects($this->exactly(4))->method('getSku')->willReturn($sku); + $this->basePriceInterface->expects($this->exactly(5))->method('getSku')->willReturn($sku); + $this->invalidSkuChecker->expects($this->once())->method('retrieveInvalidSkuList')->willReturn([]); + $this->validationResult->expects($this->once())->method('getFailedRowIds')->willReturn([]); $this->productIdLocator - ->expects($this->exactly(2)) + ->expects($this->exactly(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->basePriceInterface->expects($this->exactly(2))->method('getStoreId')->willReturn(1); $this->pricePersistence->expects($this->atLeastOnce())->method('getEntityLinkField')->willReturn('row_id'); @@ -283,44 +289,71 @@ class BasePriceStorageTest extends \PHPUnit_Framework_TestCase ] ]; $this->pricePersistence->expects($this->once())->method('update')->with($formattedPrices); - $this->assertTrue($this->model->update([$this->basePriceInterface])); + $this->validationResult->expects($this->any())->method('getFailedItems')->willReturn([]); + $this->assertEquals([], $this->model->update([1 => $this->basePriceInterface])); } /** * Test update method without SKU. * - * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage Invalid attribute sku: . + * @return void */ public function testUpdateWithoutSku() { - $this->basePriceInterface->expects($this->exactly(2))->method('getSku')->willReturn(null); - $this->model->update([$this->basePriceInterface]); + $this->basePriceInterface->expects($this->exactly(3))->method('getSku')->willReturn(null); + $this->validationResult->expects($this->once())->method('getFailedRowIds')->willReturn([0 => 0]); + $this->pricePersistenceFactory + ->expects($this->once()) + ->method('create') + ->with(['attributeCode' => 'price']) + ->willReturn($this->pricePersistence); + $priceUpdateResult = $this->getMockForAbstractClass( + \Magento\Catalog\Api\Data\PriceUpdateResultInterface::class, + [], + '', + false, + true, + true, + [] + ); + + $this->validationResult->expects($this->any())->method('getFailedItems')->willReturn([$priceUpdateResult]); + $this->assertEquals( + [$priceUpdateResult], + $this->model->update([$this->basePriceInterface]) + ); } /** * Test update method with negative price. * - * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage Invalid attribute Price: -15. + * @return void */ 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->invalidSkuChecker->expects($this->once())->method('retrieveInvalidSkuList')->willReturn([]); + $this->validationResult->expects($this->once())->method('getFailedRowIds')->willReturn([0 => 0]); + $this->basePriceInterface->expects($this->exactly(3))->method('getSku')->willReturn($sku); $this->basePriceInterface->expects($this->exactly(3))->method('getPrice')->willReturn(-15); - $this->model->update([$this->basePriceInterface]); + $priceUpdateResult = $this->getMockForAbstractClass( + \Magento\Catalog\Api\Data\PriceUpdateResultInterface::class, + [], + '', + false, + true, + true, + [] + ); + $this->pricePersistenceFactory + ->expects($this->once()) + ->method('create') + ->with(['attributeCode' => 'price']) + ->willReturn($this->pricePersistence); + $this->validationResult->expects($this->any())->method('getFailedItems')->willReturn([$priceUpdateResult]); + $this->assertEquals( + [$priceUpdateResult], + $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 index 16a7a2407c3deffe82efe084400a6ede1a172009..78f6c6540b6cb3487711c2d70949b2039ead5b14 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/CostStorageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/CostStorageTest.php @@ -41,6 +41,16 @@ class CostStorageTest extends \PHPUnit_Framework_TestCase */ private $storeRepository; + /** + * @var \Magento\Catalog\Model\Product\Price\InvalidSkuChecker|\PHPUnit_Framework_MockObject_MockObject + */ + private $invalidSkuChecker; + + /** + * @var \Magento\Catalog\Model\Product\Price\Validation\Result|\PHPUnit_Framework_MockObject_MockObject + */ + private $validationResult; + /** * @var \Magento\Catalog\Model\Product\Price\CostStorage */ @@ -101,6 +111,12 @@ class CostStorageTest extends \PHPUnit_Framework_TestCase true, ['getById'] ); + $this->validationResult = $this->getMockBuilder(\Magento\Catalog\Model\Product\Price\Validation\Result::class) + ->disableOriginalConstructor() + ->getMock(); + $this->invalidSkuChecker = $this->getMockBuilder(\Magento\Catalog\Model\Product\Price\InvalidSkuChecker::class) + ->disableOriginalConstructor() + ->getMock(); $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->model = $objectManager->getObject( @@ -110,6 +126,8 @@ class CostStorageTest extends \PHPUnit_Framework_TestCase 'costInterfaceFactory' => $this->costInterfaceFactory, 'productIdLocator' => $this->productIdLocator, 'storeRepository' => $this->storeRepository, + 'validationResult' => $this->validationResult, + 'invalidSkuChecker' => $this->invalidSkuChecker, 'allowedProductTypes' => ['simple', 'virtual', 'downloadable'], ] ); @@ -123,16 +141,6 @@ class CostStorageTest extends \PHPUnit_Framework_TestCase 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, @@ -145,10 +153,9 @@ class CostStorageTest extends \PHPUnit_Framework_TestCase 'store_id' => 1 ] ]; - $this->productIdLocator - ->expects($this->once()) - ->method('retrieveProductIdsBySkus')->with($skus) - ->willReturn($idsBySku); + $this->invalidSkuChecker + ->expects($this->atLeastOnce()) + ->method('isSkuListValid'); $this->pricePersistenceFactory ->expects($this->once()) ->method('create') @@ -182,32 +189,6 @@ class CostStorageTest extends \PHPUnit_Framework_TestCase $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. * @@ -228,12 +209,16 @@ class CostStorageTest extends \PHPUnit_Framework_TestCase 1 => \Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL ] ]; - $this->costInterface->expects($this->exactly(4))->method('getSku')->willReturn($sku); + $this->costInterface->expects($this->exactly(5))->method('getSku')->willReturn($sku); $this->productIdLocator - ->expects($this->exactly(2)) + ->expects($this->exactly(1)) ->method('retrieveProductIdsBySkus')->with([$sku]) ->willReturn($idsBySku); $this->costInterface->expects($this->exactly(3))->method('getCost')->willReturn(15); + $this->invalidSkuChecker + ->expects($this->exactly(1)) + ->method('retrieveInvalidSkuList') + ->willReturn([]); $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); @@ -242,6 +227,10 @@ class CostStorageTest extends \PHPUnit_Framework_TestCase ->method('create') ->with(['attributeCode' => 'cost']) ->willReturn($this->pricePersistence); + $this->validationResult + ->expects($this->exactly(1)) + ->method('getFailedRowIds') + ->willReturn([]); $formattedPrices = [ [ 'store_id' => 1, @@ -250,42 +239,38 @@ class CostStorageTest extends \PHPUnit_Framework_TestCase ] ]; $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]); + $this->validationResult + ->expects($this->exactly(1)) + ->method('getFailedItems') + ->willReturn([]); + $this->assertEmpty($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('getSku')->willReturn($sku); + $this->invalidSkuChecker + ->expects($this->exactly(1)) + ->method('retrieveInvalidSkuList') + ->willReturn([]); + $this->validationResult + ->expects($this->exactly(1)) + ->method('getFailedRowIds') + ->willReturn([0]); + $this->pricePersistenceFactory + ->expects($this->once()) + ->method('create') + ->with(['attributeCode' => 'cost']) + ->willReturn($this->pricePersistence); + $this->pricePersistence->expects($this->atLeastOnce())->method('update'); $this->costInterface->expects($this->exactly(3))->method('getCost')->willReturn(-15); + $this->validationResult + ->expects($this->atLeastOnce()) + ->method('getFailedItems'); $this->model->update([$this->costInterface]); } @@ -297,26 +282,16 @@ class CostStorageTest extends \PHPUnit_Framework_TestCase 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->invalidSkuChecker + ->expects($this->exactly(1)) + ->method('isSkuListValid') + ->willReturn(true); $this->model->delete($skus); } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/InvalidSkuCheckerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/InvalidSkuCheckerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..30131e553f54433dc16375d980dae6210cee5380 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/InvalidSkuCheckerTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Model\Product\Price; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * Test for model Product\Price\InvalidSkuChecker. + */ +class InvalidSkuCheckerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var \Magento\Catalog\Model\Product\Price\InvalidSkuChecker + */ + private $invalidSkuChecker; + + /** + * @var \Magento\Catalog\Model\ProductIdLocatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productIdLocator; + + /** + * @var \Magento\Catalog\Api\ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productRepository; + + /** + * {@inheritdoc} + */ + protected function setUp() + { + $this->productIdLocator = $this->getMockBuilder(\Magento\Catalog\Model\ProductIdLocatorInterface::class) + ->setMethods(['retrieveProductIdsBySkus']) + ->disableOriginalConstructor()->getMockForAbstractClass(); + + $this->productRepository = $this->getMockBuilder(\Magento\Catalog\Api\ProductRepositoryInterface::class) + ->setMethods(['get']) + ->disableOriginalConstructor()->getMockForAbstractClass(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->invalidSkuChecker = $this->objectManagerHelper->getObject( + \Magento\Catalog\Model\Product\Price\InvalidSkuChecker::class, + [ + 'productIdLocator' => $this->productIdLocator, + 'productRepository' => $this->productRepository + ] + ); + } + + /** + * Prepare retrieveInvalidSkuList(). + * + * @param string $productType + * @param string $productSku + * @return void + */ + private function prepareRetrieveInvalidSkuListMethod($productType, $productSku) + { + $idsBySku = [$productSku => [235235235 => $productType]]; + $this->productIdLocator->expects($this->atLeastOnce())->method('retrieveProductIdsBySkus') + ->willReturn($idsBySku); + + $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) + ->setMethods(['getPriceType']) + ->disableOriginalConstructor()->getMockForAbstractClass(); + $productPriceType = 0; + $product->expects($this->atLeastOnce())->method('getPriceType')->willReturn($productPriceType); + + $this->productRepository->expects($this->atLeastOnce())->method('get')->willReturn($product); + } + + /** + * Test for retrieveInvalidSkuList(). + * + * @return void + */ + public function testRetrieveInvalidSkuList() + { + $productSku = 'LKJKJ2233636'; + $productType = \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE; + $methodParamSku = 'SDFSDF3242355'; + $skus = [$methodParamSku]; + $allowedProductTypes = [$productType]; + $allowedPriceTypeValue = true; + + $this->prepareRetrieveInvalidSkuListMethod($productType, $productSku); + + $expects = [$methodParamSku, $productSku]; + $result = $this->invalidSkuChecker->retrieveInvalidSkuList($skus, $allowedProductTypes, $allowedPriceTypeValue); + $this->assertEquals($expects, $result); + } + + /** + * Test for isSkuListValid() with Exception. + * + * @expectedException \Magento\Framework\Exception\NoSuchEntityException + * @return void + */ + public function testIsSkuListValidWithException() + { + $productSku = 'LKJKJ2233636'; + $productType = \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE; + $methodParamSku = 'SDFSDF3242355'; + $skus = [$methodParamSku]; + $allowedProductTypes = [$productType]; + $allowedPriceTypeValue = true; + + $this->prepareRetrieveInvalidSkuListMethod($productType, $productSku); + + $this->invalidSkuChecker->isSkuListValid($skus, $allowedProductTypes, $allowedPriceTypeValue); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/SpecialPriceStorageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/SpecialPriceStorageTest.php new file mode 100644 index 0000000000000000000000000000000000000000..3a8368b1c096aae4b288610f335e4c20756623ed --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/SpecialPriceStorageTest.php @@ -0,0 +1,294 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Model\Product\Price; + +/** + * Test for SpecialPriceStorage model. + */ +class SpecialPriceStorageTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Catalog\Api\SpecialPriceInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $specialPriceResource; + + /** + * @var \Magento\Catalog\Api\Data\SpecialPriceInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $specialPriceFactory; + + /** + * @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\InvalidSkuChecker|\PHPUnit_Framework_MockObject_MockObject + */ + private $invalidSkuChecker; + + /** + * @var \Magento\Catalog\Model\Product\Price\Validation\Result|\PHPUnit_Framework_MockObject_MockObject + */ + private $validationResult; + + /** + * @var \Magento\Catalog\Model\Product\Price\SpecialPriceStorage + */ + private $model; + + /** + * Set up. + * + * @return void + */ + protected function setUp() + { + $this->specialPriceResource = $this->getMockBuilder(\Magento\Catalog\Api\SpecialPriceInterface::class) + ->disableOriginalConstructor()->setMethods(['get', 'update', 'delete', 'getEntityLinkField'])->getMock(); + $this->productIdLocator = $this->getMockBuilder(\Magento\Catalog\Model\ProductIdLocatorInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->storeRepository = $this->getMockBuilder(\Magento\Store\Api\StoreRepositoryInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->invalidSkuChecker = $this->getMockBuilder(\Magento\Catalog\Model\Product\Price\InvalidSkuChecker::class) + ->disableOriginalConstructor()->getMock(); + $this->validationResult = $this->getMockBuilder(\Magento\Catalog\Model\Product\Price\Validation\Result::class) + ->disableOriginalConstructor()->getMock(); + $this->specialPriceFactory = $this->getMockBuilder( + \Magento\Catalog\Api\Data\SpecialPriceInterfaceFactory::class + )->disableOriginalConstructor()->setMethods(['create'])->getMock(); + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->model = $objectManager->getObject( + \Magento\Catalog\Model\Product\Price\SpecialPriceStorage::class, + [ + 'specialPriceResource' => $this->specialPriceResource, + 'specialPriceFactory' => $this->specialPriceFactory, + 'productIdLocator' => $this->productIdLocator, + 'storeRepository' => $this->storeRepository, + 'invalidSkuChecker' => $this->invalidSkuChecker, + 'validationResult' => $this->validationResult, + ] + ); + } + + /** + * Test get method. + * + * @return void + */ + public function testGet() + { + $skus = ['sku_1', 'sku_2']; + $rawPrices = [ + [ + 'entity_id' => 1, + 'value' => 15, + 'store_id' => 1, + 'sku' => 'sku_1', + 'price_from' => '2016-12-20 01:02:03', + 'price_to' => '2016-12-21 01:02:03', + ], + [ + 'entity_id' => 2, + 'value' => 15, + 'store_id' => 1, + 'price_from' => '2016-12-20 01:02:03', + 'price_to' => '2016-12-21 01:02:03', + ], + [ + 'entity_id' => 3, + 'value' => 15, + 'store_id' => 1, + 'price_from' => '2016-12-20 01:02:03', + 'price_to' => '2016-12-21 01:02:03', + ], + ]; + $this->specialPriceResource->expects($this->once())->method('get')->willReturn($rawPrices); + $this->specialPriceResource->expects($this->atLeastOnce()) + ->method('getEntityLinkField')->willReturn('entity_id'); + + $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\SpecialPriceInterface::class) + ->disableOriginalConstructor()->getMock(); + $price->expects($this->exactly(3))->method('setPrice'); + $this->specialPriceFactory->expects($this->atLeastOnce())->method('create')->willReturn($price); + $this->productIdLocator->expects($this->atLeastOnce())->method('retrieveProductIdsBySkus')->willReturn( + [ + 'sku_2' => [2 => 'prod'] + ] + ); + $this->model->get($skus); + } + + /** + * Test update method. + * + * @return void + */ + public function testUpdate() + { + $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\SpecialPriceInterface::class) + ->disableOriginalConstructor()->getMock(); + + $prices = [1 => $price]; + $price->expects($this->atLeastOnce())->method('getSku')->willReturn('sku_1'); + $price->expects($this->atLeastOnce())->method('getPrice')->willReturn(15); + $price->expects($this->atLeastOnce())->method('getStoreId')->willReturn(1); + $price->expects($this->atLeastOnce())->method('getPriceFrom')->willReturn('2016-12-20 01:02:03'); + $price->expects($this->atLeastOnce())->method('getPriceTo')->willReturn('2016-12-21 01:02:03'); + + $this->invalidSkuChecker->expects($this->once())->method('retrieveInvalidSkuList')->willReturn([]); + $this->storeRepository->expects($this->once())->method('getById'); + $this->validationResult->expects($this->never())->method('addFailedItem'); + $this->validationResult->expects($this->atLeastOnce())->method('getFailedRowIds')->willReturn([]); + + $this->specialPriceResource->expects($this->once())->method('update')->with($prices); + + $this->model->update($prices); + } + + /** + * Test update method with invalid sku. + * + * @return void + */ + public function testUpdateWithInvalidSku() + { + $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\SpecialPriceInterface::class) + ->disableOriginalConstructor()->getMock(); + $prices = [1 => $price]; + + $price->expects($this->atLeastOnce())->method('getSku')->willReturn('sku_1'); + $price->expects($this->atLeastOnce())->method('getPrice')->willReturn(15); + $price->expects($this->atLeastOnce())->method('getStoreId')->willReturn(1); + $price->expects($this->atLeastOnce())->method('getPriceFrom')->willReturn('2016-12-20 01:02:03'); + $price->expects($this->atLeastOnce())->method('getPriceTo')->willReturn('2016-12-21 01:02:03'); + + $this->invalidSkuChecker->expects($this->once())->method('retrieveInvalidSkuList')->willReturn(['sku_1']); + $this->storeRepository->expects($this->once())->method('getById'); + $this->validationResult->expects($this->once())->method('addFailedItem')->with(1); + $this->validationResult->expects($this->atLeastOnce())->method('getFailedRowIds')->willReturn([1]); + + $this->specialPriceResource->expects($this->once())->method('update')->with([]); + + $this->model->update($prices); + } + + /** + * Test update method with price = null. + * + * @return void + */ + public function testUpdateWithoutPrice() + { + $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\SpecialPriceInterface::class) + ->disableOriginalConstructor()->getMock(); + $prices = [1 => $price]; + + $price->expects($this->atLeastOnce())->method('getSku')->willReturn('sku_1'); + $price->expects($this->atLeastOnce())->method('getPrice')->willReturn(null); + $price->expects($this->atLeastOnce())->method('getStoreId')->willReturn(1); + $price->expects($this->atLeastOnce())->method('getPriceFrom')->willReturn('2016-12-20 01:02:03'); + $price->expects($this->atLeastOnce())->method('getPriceTo')->willReturn('2016-12-21 01:02:03'); + + $this->invalidSkuChecker->expects($this->once())->method('retrieveInvalidSkuList')->willReturn([]); + $this->storeRepository->expects($this->once())->method('getById'); + $this->validationResult->expects($this->once())->method('addFailedItem')->with(1); + $this->validationResult->expects($this->atLeastOnce())->method('getFailedRowIds')->willReturn([1]); + + $this->specialPriceResource->expects($this->once())->method('update')->with([]); + + $this->model->update($prices); + } + + /** + * Test update method with price = null. + * + * @return void + */ + public function testUpdateWithException() + { + $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\SpecialPriceInterface::class) + ->disableOriginalConstructor()->getMock(); + $prices = [1 => $price]; + + $price->expects($this->atLeastOnce())->method('getSku')->willReturn('sku_1'); + $price->expects($this->atLeastOnce())->method('getPrice')->willReturn(15); + $price->expects($this->atLeastOnce())->method('getStoreId')->willReturn(1); + $price->expects($this->atLeastOnce())->method('getPriceFrom')->willReturn('2016-12-20 01:02:03'); + $price->expects($this->atLeastOnce())->method('getPriceTo')->willReturn('2016-12-21 01:02:03'); + + $this->invalidSkuChecker->expects($this->once())->method('retrieveInvalidSkuList')->willReturn([]); + $this->storeRepository->expects($this->once())->method('getById') + ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException()); + $this->validationResult->expects($this->once())->method('addFailedItem')->with(1); + $this->validationResult->expects($this->atLeastOnce())->method('getFailedRowIds')->willReturn([1]); + + $this->specialPriceResource->expects($this->once())->method('update')->with([]); + + $this->model->update($prices); + } + + /** + * Test update method with incorrect price_from field. + * + * @return void + */ + public function testUpdateWithIncorrectPriceFrom() + { + $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\SpecialPriceInterface::class) + ->disableOriginalConstructor()->getMock(); + $prices = [1 => $price]; + + $price->expects($this->atLeastOnce())->method('getSku')->willReturn('sku_1'); + $price->expects($this->atLeastOnce())->method('getPrice')->willReturn(15); + $price->expects($this->atLeastOnce())->method('getStoreId')->willReturn(1); + $price->expects($this->atLeastOnce())->method('getPriceFrom')->willReturn('incorrect'); + $price->expects($this->atLeastOnce())->method('getPriceTo')->willReturn('2016-12-21 01:02:03'); + + $this->invalidSkuChecker->expects($this->once())->method('retrieveInvalidSkuList')->willReturn([]); + $this->storeRepository->expects($this->once())->method('getById'); + $this->validationResult->expects($this->once())->method('addFailedItem')->with(1); + $this->validationResult->expects($this->atLeastOnce())->method('getFailedRowIds')->willReturn([1]); + + $this->specialPriceResource->expects($this->once())->method('update')->with([]); + + $this->model->update($prices); + } + + /** + * Test update method with incorrect price_to field. + * + * @return void + */ + public function testUpdateWithIncorrectPriceTo() + { + $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\SpecialPriceInterface::class) + ->disableOriginalConstructor()->getMock(); + $prices = [1 => $price]; + + $price->expects($this->atLeastOnce())->method('getSku')->willReturn('sku_1'); + $price->expects($this->atLeastOnce())->method('getPrice')->willReturn(15); + $price->expects($this->atLeastOnce())->method('getStoreId')->willReturn(1); + $price->expects($this->atLeastOnce())->method('getPriceFrom')->willReturn('2016-12-21 01:02:03'); + $price->expects($this->atLeastOnce())->method('getPriceTo')->willReturn('incorrect'); + + $this->invalidSkuChecker->expects($this->once())->method('retrieveInvalidSkuList')->willReturn([]); + $this->storeRepository->expects($this->once())->method('getById'); + $this->validationResult->expects($this->once())->method('addFailedItem')->with(1); + $this->validationResult->expects($this->atLeastOnce())->method('getFailedRowIds')->willReturn([1]); + + $this->specialPriceResource->expects($this->once())->method('update')->with([]); + + $this->model->update($prices); + } +} 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 index b440ee21f41e5e4a7601597fb4a8043f4a09c6ab..398b340da08cd10e22c69a3ef63553559f2f7af4 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php @@ -17,7 +17,7 @@ class TierPriceStorageTest extends \PHPUnit_Framework_TestCase private $tierPricePersistence; /** - * @var \Magento\Catalog\Model\Product\Price\TierPriceValidator|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator|\PHPUnit_Framework_MockObject_MockObject */ private $tierPriceValidator; @@ -51,6 +51,11 @@ class TierPriceStorageTest extends \PHPUnit_Framework_TestCase */ private $tierPriceStorage; + /** + * @var \Magento\Catalog\Model\Product\Price\InvalidSkuChecker|\PHPUnit_Framework_MockObject_MockObject + */ + private $invalidSkuChecker; + /** * {@inheritdoc} */ @@ -67,7 +72,7 @@ class TierPriceStorageTest extends \PHPUnit_Framework_TestCase ->method('getEntityLinkField') ->willReturn('row_id'); $this->tierPriceValidator = $this->getMock( - \Magento\Catalog\Model\Product\Price\TierPriceValidator::class, + \Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator::class, [], [], '', @@ -108,6 +113,10 @@ class TierPriceStorageTest extends \PHPUnit_Framework_TestCase '', false ); + $this->invalidSkuChecker = $this->getMockBuilder(\Magento\Catalog\Model\Product\Price\InvalidSkuChecker::class) + ->disableOriginalConstructor() + ->getMock(); + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->tierPriceStorage = $objectManager->getObject( \Magento\Catalog\Model\Product\Price\TierPriceStorage::class, @@ -118,6 +127,7 @@ class TierPriceStorageTest extends \PHPUnit_Framework_TestCase 'priceIndexer' => $this->priceIndexer, 'productIdLocator' => $this->productIdLocator, 'config' => $this->config, + 'invalidSkuChecker' => $this->invalidSkuChecker, 'typeList' => $this->typeList, ] ); @@ -174,10 +184,18 @@ class TierPriceStorageTest extends \PHPUnit_Framework_TestCase */ public function testUpdate() { + $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\TierPriceInterface::class)->getMockForAbstractClass(); + $result = $this->getMockBuilder(\Magento\Catalog\Model\Product\Price\Validation\Result::class) + ->disableOriginalConstructor() + ->getMock(); + $result->expects($this->atLeastOnce())->method('getFailedRowIds')->willReturn([]); $this->productIdLocator->expects($this->atLeastOnce()) ->method('retrieveProductIdsBySkus') - ->willReturn(['bundle' => ['2' => 'bundle']]); - $this->tierPriceValidator->expects($this->atLeastOnce())->method('validatePrices')->willReturn(true); + ->willReturn(['simple' => ['2' => 'simple'], 'virtual' => ['3' => 'virtual']]); + $this->tierPriceValidator + ->expects($this->atLeastOnce()) + ->method('retrieveValidationResult') + ->willReturn($result); $this->tierPriceFactory->expects($this->atLeastOnce())->method('createSkeleton')->willReturn( [ 'row_id' => 2, @@ -209,9 +227,8 @@ class TierPriceStorageTest extends \PHPUnit_Framework_TestCase $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])); + $price->method('getSku')->willReturn('simple'); + $this->assertEmpty($this->tierPriceStorage->update([$price])); } /** @@ -220,10 +237,20 @@ class TierPriceStorageTest extends \PHPUnit_Framework_TestCase */ public function testReplace() { - $this->tierPriceValidator->expects($this->atLeastOnce())->method('validatePrices'); + $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\TierPriceInterface::class)->getMockForAbstractClass(); + $price->method('getSku')->willReturn('virtual'); + $result = $this->getMockBuilder(\Magento\Catalog\Model\Product\Price\Validation\Result::class) + ->disableOriginalConstructor() + ->getMock(); + $result->expects($this->atLeastOnce())->method('getFailedRowIds')->willReturn([]); $this->productIdLocator->expects($this->atLeastOnce()) ->method('retrieveProductIdsBySkus') - ->willReturn(['virtual' => ['2' => 'virtual']]); + ->willReturn(['simple' => ['2' => 'simple'], 'virtual' => ['3' => 'virtual']]); + + $this->tierPriceValidator + ->expects($this->atLeastOnce()) + ->method('retrieveValidationResult') + ->willReturn($result); $this->tierPriceFactory->expects($this->atLeastOnce())->method('createSkeleton')->willReturn( [ 'row_id' => 3, @@ -237,11 +264,9 @@ class TierPriceStorageTest extends \PHPUnit_Framework_TestCase ); $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])); + $this->assertEmpty($this->tierPriceStorage->replace([$price])); } /** @@ -250,7 +275,15 @@ class TierPriceStorageTest extends \PHPUnit_Framework_TestCase */ public function testDelete() { - $this->tierPriceValidator->expects($this->atLeastOnce())->method('validatePrices'); + $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\TierPriceInterface::class)->getMockForAbstractClass(); + $price->method('getSku')->willReturn('simple'); + $result = $this->getMockBuilder(\Magento\Catalog\Model\Product\Price\Validation\Result::class) + ->disableOriginalConstructor() + ->getMock(); + $result->expects($this->atLeastOnce())->method('getFailedRowIds')->willReturn([]); + $this->tierPriceValidator->expects($this->atLeastOnce()) + ->method('retrieveValidationResult') + ->willReturn($result); $this->productIdLocator->expects($this->atLeastOnce()) ->method('retrieveProductIdsBySkus') ->willReturn(['simple' => ['2' => 'simple']]); @@ -285,8 +318,6 @@ class TierPriceStorageTest extends \PHPUnit_Framework_TestCase $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])); + $this->assertEmpty($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 deleted file mode 100644 index 8373098593634d27bf21534dd510bd592d3238ca..0000000000000000000000000000000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceValidatorTest.php +++ /dev/null @@ -1,471 +0,0 @@ -<?php -/** - * Copyright © 2013-2017 Magento, Inc. 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/Product/Price/Validation/TierPriceValidatorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/Validation/TierPriceValidatorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..dfa75595ff9954c54d009495a55ae7b8d66e3486 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/Validation/TierPriceValidatorTest.php @@ -0,0 +1,333 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Model\Product\Price\Validation; + +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\InvalidSkuChecker|\PHPUnit_Framework_MockObject_MockObject + */ + private $invalidSkuChecker; + + /** + * @var \Magento\Catalog\Model\Product\Price\Validation\Result|\PHPUnit_Framework_MockObject_MockObject + */ + private $validationResult; + + /** + * @var \Magento\Catalog\Model\Product\Price\Validation\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->filterBuilder->method('setField')->willReturnSelf(); + $this->filterBuilder->method('setValue')->willReturnSelf(); + $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'] + ); + $this->validationResult = $this->getMockBuilder(\Magento\Catalog\Model\Product\Price\Validation\Result::class) + ->disableOriginalConstructor() + ->getMock(); + $this->invalidSkuChecker = $this->getMockBuilder(\Magento\Catalog\Model\Product\Price\InvalidSkuChecker::class) + ->disableOriginalConstructor() + ->getMock(); + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->model = $objectManager->getObject( + \Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator::class, + [ + 'productIdLocator' => $this->productIdLocator, + 'searchCriteriaBuilder' => $this->searchCriteriaBuilder, + 'filterBuilder' => $this->filterBuilder, + 'customerGroupRepository' => $this->customerGroupRepository, + 'websiteRepository' => $this->websiteRepository, + 'tierPricePersistence' => $this->tierPricePersistence, + 'validationResult' => $this->validationResult, + 'invalidSkuChecker' => $this->invalidSkuChecker, + 'allowedProductTypes' => ['simple', 'virtual', 'bundle', 'downloadable'], + ] + ); + } + + /** + * Test retrieveValidPrices method. + * + * @return void + */ + public function testRetrieveValidPrices() + { + $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->productIdLocator->expects($this->once())->method('retrieveProductIdsBySkus')->willReturn($idsBySku); + $productPrice = 15; + $this->tierPriceInterface->expects($this->exactly(10))->method('getSku')->willReturn($sku); + $this->invalidSkuChecker->expects($this->once())->method('retrieveInvalidSkuList')->willReturn([]); + $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->retrieveValidationResult([$this->tierPriceInterface], []); + } + + /** + * Test retrieveValidPrices method with downloadable product. + */ + public function testRetrieveValidPricesWithDownloadableProduct() + { + $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')->willReturn($idsBySku); + $this->tierPriceInterface->expects($this->exactly(10))->method('getSku')->willReturn('sku_1'); + $this->invalidSkuChecker->expects($this->once())->method('retrieveInvalidSkuList')->willReturn([]); + $productPrice = 15; + $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->validationResult + ->expects($this->never()) + ->method('addFailedItem'); + $this->model->retrieveValidationResult([$this->tierPriceInterface], []); + } + + /** + * Test method call with invalid values. + */ + public function testRetrieveValidPricesWithInvalidCall() + { + $idsBySku = [ + 'sku_1' => [1 => \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE], + 'sku_2' => [2 => \Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL], + 'invalid' => [3 => \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE], + ]; + + $this->tierPriceInterface->expects($this->exactly(10))->method('getSku')->willReturn('sku_1'); + $this->invalidSkuChecker->expects($this->once())->method('retrieveInvalidSkuList')->willReturn(['invalid']); + $this->productIdLocator->expects($this->once())->method('retrieveProductIdsBySkus')->willReturn($idsBySku); + $this->validationResult + ->expects($this->exactly(5)) + ->method('addFailedItem'); + $this->tierPriceInterface->expects($this->exactly(3))->method('getPrice')->willReturn('-90'); + $this->tierPriceInterface->expects($this->exactly(2))->method('getPriceType')->willReturn('unknown'); + $this->tierPriceInterface->expects($this->exactly(4))->method('getQuantity')->willReturn('-90'); + $this->websiteRepository->method('getById') + ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException()); + $searchCriteria = $this->getMockForAbstractClass( + \Magento\Framework\Api\SearchCriteriaInterface::class, + [], + '', + false, + true, + true, + ['create'] + ); + $searchCriteria->method('create')->willReturnSelf(); + $this->searchCriteriaBuilder->method('addFilters')->willReturn($searchCriteria); + $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')->willReturnSelf(); + $searchResults = $this->getMockForAbstractClass( + \Magento\Customer\Api\Data\GroupSearchResultsInterface::class, + [], + '', + false, + true, + true, + ['getItems'] + ); + $this->filterBuilder->expects($this->atLeastOnce())->method('create')->willReturnSelf(); + $searchResults->expects($this->atLeastOnce())->method('getItems')->willReturn([]); + $this->customerGroupRepository + ->expects($this->atLeastOnce()) + ->method('getList') + ->willReturn($searchResults); + $this->model->retrieveValidationResult([$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/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index 83f194bc9a2b59fd206e4678803c357966d7fdd7..023b39ddc37da1129a625835c965d2d569536bc0 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -57,6 +57,10 @@ <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\Api\SpecialPriceStorageInterface" type="Magento\Catalog\Model\Product\Price\SpecialPriceStorage" /> + <preference for="Magento\Catalog\Api\Data\SpecialPriceInterface" type="Magento\Catalog\Model\Product\Price\SpecialPrice" /> + <preference for="Magento\Catalog\Api\Data\PriceUpdateResultInterface" type="Magento\Catalog\Model\Product\Price\PriceUpdateResult" /> + <preference for="Magento\Catalog\Api\SpecialPriceInterface" type="Magento\Catalog\Model\ResourceModel\Product\Price\SpecialPrice" /> <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" /> @@ -872,7 +876,7 @@ </argument> </arguments> </type> - <type name="Magento\Catalog\Model\Product\Price\TierPriceValidator"> + <type name="Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator"> <arguments> <argument name="allowedProductTypes" xsi:type="array"> <item name="0" xsi:type="string">simple</item> @@ -881,4 +885,12 @@ </argument> </arguments> </type> + <type name="Magento\Catalog\Model\Product\Price\SpecialPriceStorage"> + <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> </config> diff --git a/app/code/Magento/Catalog/etc/webapi.xml b/app/code/Magento/Catalog/etc/webapi.xml index eb1167b64679df9b3f6ee126525fe51589106b83..1101a7bc93814917c3ccb95a5b60a65d846c6729 100644 --- a/app/code/Magento/Catalog/etc/webapi.xml +++ b/app/code/Magento/Catalog/etc/webapi.xml @@ -300,6 +300,24 @@ <resource ref="Magento_Catalog::catalog"/> </resources> </route> + <route url="/V1/products/special-price-information" method="POST"> + <service class="Magento\Catalog\Api\SpecialPriceStorageInterface" method="get"/> + <resources> + <resource ref="Magento_Catalog::catalog"/> + </resources> + </route> + <route url="/V1/products/special-price" method="POST"> + <service class="Magento\Catalog\Api\SpecialPriceStorageInterface" method="update"/> + <resources> + <resource ref="Magento_Catalog::catalog"/> + </resources> + </route> + <route url="/V1/products/special-price-delete" method="POST"> + <service class="Magento\Catalog\Api\SpecialPriceStorageInterface" 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/Downloadable/etc/di.xml b/app/code/Magento/Downloadable/etc/di.xml index 62e15cddca4b53d401069989f791f762758fce72..6b5627ead2ec8c9b68e08b8ccce80e282a1fb45a 100644 --- a/app/code/Magento/Downloadable/etc/di.xml +++ b/app/code/Magento/Downloadable/etc/di.xml @@ -130,7 +130,7 @@ </argument> </arguments> </type> - <type name="Magento\Catalog\Model\Product\Price\TierPriceValidator"> + <type name="Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator"> <arguments> <argument name="allowedProductTypes" xsi:type="array"> <item name="3" xsi:type="string">downloadable</item> @@ -144,4 +144,11 @@ </argument> </arguments> </type> + <type name="Magento\Catalog\Model\Product\Price\SpecialPriceStorage"> + <arguments> + <argument name="allowedProductTypes" xsi:type="array"> + <item name="3" xsi:type="string">downloadable</item> + </argument> + </arguments> + </type> </config> diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/BasePriceStorageTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/BasePriceStorageTest.php index f9bbbd92153e916fbd6934a7946c72e6e7737769..3da02007eb5082fa8d056486670a8ec077554645 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/BasePriceStorageTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/BasePriceStorageTest.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Api; use Magento\TestFramework\TestCase\WebapiAbstract; +use Magento\Framework\Webapi\Exception as HTTPExceptionCodes; /** * BasePriceStorage test. @@ -58,6 +59,40 @@ class BasePriceStorageTest extends WebapiAbstract $this->assertEquals($product->getSku(), $response[0]['sku']); } + /** + * Test get method, called with not existing SKU. + */ + public function testGetWithInvalidSku() + { + $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', + ], + ]; + $expected = 'Requested product doesn\'t exist: %sku'; + + try { + $this->_webApiCall($serviceInfo, ['skus' => ['sku_of_not_exiting_product']]); + $this->fail("Expected throwing exception"); + } catch (\SoapFault $e) { + $this->assertContains( + $expected, + $e->getMessage(), + "SoapFault does not contain expected message." + ); + } catch (\Exception $e) { + $error = $this->processRestExceptionResult($e); + $this->assertEquals($expected, $error['message']); + $this->assertEquals(HTTPExceptionCodes::HTTP_NOT_FOUND, $e->getCode()); + } + } + /** * Test update method. * @@ -94,7 +129,62 @@ class BasePriceStorageTest extends WebapiAbstract /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ $product = $productRepository->get(self::SIMPLE_PRODUCT_SKU); - $this->assertNotEmpty($response); + $this->assertEmpty($response); $this->assertEquals($product->getPrice(), $newPrice); } + + /** + * Test update method call with invalid parameters. + */ + public function testUpdateWithInvalidParameters() + { + $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 = 9999; + $response = $this->_webApiCall( + $serviceInfo, + [ + 'prices' => [ + [ + 'sku' => 'not_existing_sku', + 'price' => $newPrice, + 'store_id' => $storeId, + ] + ] + ] + ); + + $expectedResponse = [ + 0 => [ + 'message' => 'Invalid attribute %fieldName = %fieldValue.', + 'parameters' => [ + 'SKU', + 'not_existing_sku', + ] + ], + 1 => [ + 'message' => 'Invalid attribute %fieldName = %fieldValue.', + 'parameters' => [ + 'Price', + '-9999', + ] + ], + 2 => [ + 'message' => 'Requested store is not found.', + 'parameters' => [] + ] + ]; + + $this->assertEquals($expectedResponse, $response); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CostStorageTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CostStorageTest.php index d7eda7fbd58cde9f4a317c3ee7431c166fc8eebf..ff6a4f5283ff2a5f23d48da8f813df24342cc76f 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CostStorageTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CostStorageTest.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Api; use Magento\TestFramework\TestCase\WebapiAbstract; +use Magento\Framework\Webapi\Exception as HTTPExceptionCodes; /** * CostStorage test. @@ -60,6 +61,40 @@ class CostStorageTest extends WebapiAbstract $this->assertEquals($product->getCost(), $cost); } + /** + * Test get method, called with not existing SKUs. + */ + public function testGetWithInvalidSku() + { + $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 . 'Get', + ], + ]; + $expected = 'Requested products don\'t exist: %sku'; + + try { + $this->_webApiCall($serviceInfo, ['skus' => ['sku_of_not_exiting_product', 'invalid_sku']]); + $this->fail("Expected throwing exception"); + } catch (\SoapFault $e) { + $this->assertContains( + $expected, + $e->getMessage(), + "SoapFault does not contain expected message." + ); + } catch (\Exception $e) { + $error = $this->processRestExceptionResult($e); + $this->assertEquals($expected, $error['message']); + $this->assertEquals(HTTPExceptionCodes::HTTP_NOT_FOUND, $e->getCode()); + } + } + /** * Test update method. * @@ -95,10 +130,65 @@ class CostStorageTest extends WebapiAbstract $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->assertEmpty($response); $this->assertEquals($product->getCost(), $newCost); } + /** + * Test update method call without SKU. + */ + public function testUpdateWithInvalidParameters() + { + $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', + ], + ]; + $newCost = -9999; + $storeId = 9999; + $response = $this->_webApiCall( + $serviceInfo, + [ + 'prices' => [ + [ + 'sku' => 'not_existing_sku', + 'cost' => $newCost, + 'store_id' => $storeId + ] + ] + ] + ); + + $expectedResponse = [ + 0 => [ + 'message' => 'Invalid attribute %fieldName = %fieldValue.', + 'parameters' => [ + 'SKU', + 'not_existing_sku', + ] + ], + 1 => [ + 'message' => 'Invalid attribute %fieldName = %fieldValue.', + 'parameters' => [ + 'Cost', + '-9999', + ] + ], + 2 => [ + 'message' => 'Requested store is not found.', + 'parameters' => [] + ] + ]; + + $this->assertEquals($expectedResponse, $response); + } + /** * Test delete method. * @@ -126,4 +216,38 @@ class CostStorageTest extends WebapiAbstract $this->assertTrue($response); $this->assertNull($product->getCost()); } + + /** + * Test delete method, called with not existing SKUs. + */ + public function testDeleteWithInvalidSku() + { + $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', + ], + ]; + $expectedResponseMessage = 'Requested product doesn\'t exist: %sku'; + + try { + $this->_webApiCall($serviceInfo, ['skus' => ['sku_of_not_exiting_product']]); + $this->fail("Expected throwing exception"); + } catch (\SoapFault $e) { + $this->assertContains( + $expectedResponseMessage, + $e->getMessage(), + "SoapFault does not contain expected message." + ); + } catch (\Exception $e) { + $error = $this->processRestExceptionResult($e); + $this->assertEquals($expectedResponseMessage, $error['message']); + $this->assertEquals(HTTPExceptionCodes::HTTP_NOT_FOUND, $e->getCode()); + } + } } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/SpecialPriceStorageTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/SpecialPriceStorageTest.php new file mode 100644 index 0000000000000000000000000000000000000000..bc5ebfaf68bbf53befa792c5525544da377a11cf --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/SpecialPriceStorageTest.php @@ -0,0 +1,181 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Api; + +use Magento\TestFramework\TestCase\WebapiAbstract; +use Magento\Framework\Webapi\Exception as HTTPExceptionCodes; + +/** + * SpecialPriceStorage test. + */ +class SpecialPriceStorageTest extends WebapiAbstract +{ + const SERVICE_NAME = 'catalogSpecialPriceStorageV1'; + 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() + { + $specialPrice = 3057; + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $product = $productRepository->get(self::SIMPLE_PRODUCT_SKU, true); + $product->setData('special_price', $specialPrice); + $productRepository->save($product); + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/products/special-price-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]]); + /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $product = $productRepository->get(self::SIMPLE_PRODUCT_SKU); + $this->assertNotEmpty($response); + $this->assertEquals($product->getSpecialPrice(), $response[0]['price']); + } + + /** + * Test get method, called with not existing SKUs. + */ + public function testGetWithInvalidSku() + { + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/products/special-price-information', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Get', + ], + ]; + $expected = 'Requested products don\'t exist: %sku'; + try { + $this->_webApiCall($serviceInfo, ['skus' => ['sku_of_not_exiting_product', 'invalid_sku_1']]); + $this->fail("Expected throwing exception"); + } catch (\SoapFault $e) { + $this->assertContains( + $expected, + $e->getMessage(), + "SoapFault does not contain expected message." + ); + } catch (\Exception $e) { + $error = $this->processRestExceptionResult($e); + $this->assertEquals($expected, $error['message']); + $this->assertEquals(HTTPExceptionCodes::HTTP_NOT_FOUND, $e->getCode()); + } + } + + /** + * Test update method. + * + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + */ + public function testUpdate() + { + $sku = 'virtual-product'; + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/products/special-price', + '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; + $newPrice = 31337; + $response = $this->_webApiCall( + $serviceInfo, + [ + 'prices' => [ + [ + 'price' => $newPrice, + 'store_id' => $storeId, + 'sku' => $sku, + 'price_from' => '2037-01-19 03:14:07', + 'price_to' => '2038-01-19 03:14:07', + ] + ] + ] + ); + $this->assertEmpty($response); + } + + /** + * Test delete method. + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testDelete() + { + $specialPrice = 3057; + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $fromDate = '1970-01-01 00:00:01'; + $toDate = '2038-01-19 03:14:07'; + $product = $productRepository->get(self::SIMPLE_PRODUCT_SKU, true); + $product->setData('special_price', $specialPrice) + ->setData('special_from_date', $fromDate) + ->setData('special_to_date', $toDate); + $productRepository->save($product); + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/products/special-price-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' => [ + [ + 'price' => $specialPrice, + 'store_id' => 0, + 'sku' => self::SIMPLE_PRODUCT_SKU, + 'price_from' => $fromDate, + 'price_to' => $toDate, + ] + ] + ] + ); + /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $product = $productRepository->get(self::SIMPLE_PRODUCT_SKU, false, null, true); + $this->assertEmpty($response); + $this->assertNull($product->getSpecialPrice()); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/TierPriceStorageTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/TierPriceStorageTest.php index da72fbe1005d1c6aa8e6423215cbdd27f46d3fc0..bf9d65af13e50d2d7bf36a74af82faf19b15d9b4 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/TierPriceStorageTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/TierPriceStorageTest.php @@ -93,17 +93,59 @@ class TierPriceStorageTest extends WebapiAbstract 'price_type' => \Magento\Catalog\Api\Data\TierPriceInterface::PRICE_TYPE_FIXED, 'website_id' => 0, 'sku' => self::SIMPLE_PRODUCT_SKU, - 'customer_group' => 'ALL GROUPS', + 'customer_group' => 'not logged in', '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->assertEmpty($response); $this->assertTrue($this->isPriceCorrect($newPrice, $tierPrices)); $this->assertTrue($this->isPriceCorrect($updatedPrice, $tierPrices)); } + /** + * Call update method with specifying new website value for tier price with all websites value. + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testUpdateWebsiteForAllWebsites() + { + $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', + ], + ]; + $invalidPrice = [ + 'price' => 40, + 'price_type' => \Magento\Catalog\Api\Data\TierPriceInterface::PRICE_TYPE_FIXED, + 'website_id' => 2, + 'sku' => self::SIMPLE_PRODUCT_SKU, + 'customer_group' => 'not logged in', + 'quantity' => 3 + ]; + $response = $this->_webApiCall($serviceInfo, ['prices' => [$invalidPrice]]); + $this->assertNotEmpty($response); + $this->assertEquals('Invalid attribute %fieldName = %fieldValue.', $response[0]['message']); + $this->assertEquals('Website Id', $response[0]['parameters'][0]); + $this->assertEquals('2', $response[0]['parameters'][1]); + $message = 'We found a duplicate website, tier price, customer group and quantity: %fieldName1 ' + . '= %fieldValue1, %fieldName2 = %fieldValue2, %fieldName3 = %fieldValue3.'; + $this->assertEquals($message, $response[1]['message']); + $this->assertEquals('Customer Group', $response[1]['parameters'][0]); + $this->assertEquals('NOT LOGGED IN', $response[1]['parameters'][1]); + $this->assertEquals('Website Id', $response[1]['parameters'][2]); + $this->assertEquals('0', $response[1]['parameters'][3]); + $this->assertEquals('Quantity', $response[1]['parameters'][4]); + $this->assertEquals('3.0000', $response[1]['parameters'][5]); + } + /** * Test replace method. * @@ -136,7 +178,7 @@ class TierPriceStorageTest extends WebapiAbstract 'price_type' => \Magento\Catalog\Api\Data\TierPriceInterface::PRICE_TYPE_FIXED, 'website_id' => 0, 'sku' => self::SIMPLE_PRODUCT_SKU, - 'customer_group' => 'general', + 'customer_group' => 'not logged in', 'quantity' => 33 ] ]; @@ -144,12 +186,8 @@ class TierPriceStorageTest extends WebapiAbstract $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->assertEmpty($response); $this->assertEquals(count($newPrices), count($tierPrices)); - - foreach ($newPrices as $newPrice) { - $this->assertTrue($this->isPriceCorrect($newPrice, $tierPrices)); - } } /** @@ -197,7 +235,7 @@ class TierPriceStorageTest extends WebapiAbstract $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); $tierPrices = $productRepository->get(self::SIMPLE_PRODUCT_SKU)->getTierPrices(); $tierPrice = $tierPrices[0]; - $this->assertTrue($response); + $this->assertEmpty($response); $this->assertEquals(1, count($tierPrices)); $this->assertEquals($pricesToStore, $tierPrice); } @@ -223,6 +261,7 @@ class TierPriceStorageTest extends WebapiAbstract && $tierPrice->getExtensionAttributes()->getWebsiteId() == $price['website_id'] ) { $isCorrect = true; + break; } }