diff --git a/app/code/Magento/Bundle/Api/Data/LinkInterface.php b/app/code/Magento/Bundle/Api/Data/LinkInterface.php index e16fad7f555be..4e2268eae8f9a 100644 --- a/app/code/Magento/Bundle/Api/Data/LinkInterface.php +++ b/app/code/Magento/Bundle/Api/Data/LinkInterface.php @@ -9,6 +9,24 @@ interface LinkInterface extends \Magento\Framework\Api\ExtensibleDataInterface { + const PRICE_TYPE_FIXED = 0; + const PRICE_TYPE_PERCENT = 1; + + /** + * Get the identifier + * + * @return string|null + */ + public function getId(); + + /** + * Set id + * + * @param string $id + * @return $this + */ + public function setId($id); + /** * Get linked product sku * @@ -69,21 +87,6 @@ public function getPosition(); */ public function setPosition($position); - /** - * Get is defined - * - * @return bool|null - */ - public function getIsDefined(); - - /** - * Set is defined - * - * @param bool $isDefined - * @return $this - */ - public function setIsDefined($isDefined); - /** * Get is default * diff --git a/app/code/Magento/Bundle/Api/ProductLinkManagementInterface.php b/app/code/Magento/Bundle/Api/ProductLinkManagementInterface.php index 2109cde0f95d4..0529831f4094f 100644 --- a/app/code/Magento/Bundle/Api/ProductLinkManagementInterface.php +++ b/app/code/Magento/Bundle/Api/ProductLinkManagementInterface.php @@ -11,12 +11,13 @@ interface ProductLinkManagementInterface /** * Get all children for Bundle product * - * @param string $productId + * @param string $productSku + * @param int $optionId * @return \Magento\Bundle\Api\Data\LinkInterface[] * @throws \Magento\Framework\Exception\NoSuchEntityException * @throws \Magento\Framework\Exception\InputException */ - public function getChildren($productId); + public function getChildren($productSku, $optionId = null); /** * Add child product to specified Bundle option by product sku @@ -31,10 +32,23 @@ public function getChildren($productId); */ public function addChildByProductSku($sku, $optionId, \Magento\Bundle\Api\Data\LinkInterface $linkedProduct); + /** + * @param string $sku + * @param \Magento\Bundle\Api\Data\LinkInterface $linkedProduct + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @return bool + */ + public function saveChild( + $sku, + \Magento\Bundle\Api\Data\LinkInterface $linkedProduct + ); + /** * @param \Magento\Catalog\Api\Data\ProductInterface $product * @param int $optionId - * @param Data\LinkInterface $linkedProduct + * @param \Magento\Bundle\Api\Data\LinkInterface $linkedProduct * @throws \Magento\Framework\Exception\NoSuchEntityException * @throws \Magento\Framework\Exception\CouldNotSaveException * @throws \Magento\Framework\Exception\InputException diff --git a/app/code/Magento/Bundle/Model/Link.php b/app/code/Magento/Bundle/Model/Link.php index eb99eff55e334..fef35d3261b03 100644 --- a/app/code/Magento/Bundle/Model/Link.php +++ b/app/code/Magento/Bundle/Model/Link.php @@ -16,17 +16,34 @@ class Link extends \Magento\Framework\Model\AbstractExtensibleModel implements /**#@+ * Constants */ + const KEY_ID = 'id'; const KEY_SKU = 'sku'; const KEY_OPTION_ID = 'option_id'; const KEY_QTY = 'qty'; const KEY_POSITION = 'position'; - const KEY_IS_DEFINED = 'is_defined'; const KEY_IS_DEFAULT = 'is_default'; const KEY_PRICE = 'price'; const KEY_PRICE_TYPE = 'price_type'; - const KEY_CAN_CHANGE_QUANTITY = 'can_change_quantity'; + const KEY_CAN_CHANGE_QUANTITY = 'selection_can_change_quantity'; /**#@-*/ + /** + * {@inheritdoc} + */ + public function getId() + { + return $this->getData(self::KEY_ID); + } + + /** + * {@inheritdoc} + */ + public function setId($id) + { + return $this->setData(self::KEY_ID, $id); + } + + /** * {@inheritdoc} */ @@ -59,14 +76,6 @@ public function getPosition() return $this->getData(self::KEY_POSITION); } - /** - * {@inheritdoc} - */ - public function getIsDefined() - { - return $this->getData(self::KEY_IS_DEFINED); - } - /** * {@inheritdoc} */ @@ -143,17 +152,6 @@ public function setPosition($position) return $this->setData(self::KEY_POSITION, $position); } - /** - * Set is defined - * - * @param bool $isDefined - * @return $this - */ - public function setIsDefined($isDefined) - { - return $this->setData(self::KEY_IS_DEFINED, $isDefined); - } - /** * Set is default * diff --git a/app/code/Magento/Bundle/Model/LinkManagement.php b/app/code/Magento/Bundle/Model/LinkManagement.php index fe2c0d85d7b16..5d240c0517e59 100644 --- a/app/code/Magento/Bundle/Model/LinkManagement.php +++ b/app/code/Magento/Bundle/Model/LinkManagement.php @@ -75,15 +75,18 @@ public function __construct( /** * {@inheritdoc} */ - public function getChildren($productId) + public function getChildren($productSku, $optionId = null) { - $product = $this->productRepository->get($productId); + $product = $this->productRepository->get($productSku); if ($product->getTypeId() != \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) { throw new InputException(__('Only implemented for bundle product')); } $childrenList = []; foreach ($this->getOptions($product) as $option) { + if ($optionId !== null && $option->getOptionId() != $optionId) { + continue; + } /** @var \Magento\Catalog\Model\Product $selection */ foreach ($option->getSelections() as $selection) { $childrenList[] = $this->buildLink($selection, $product); @@ -107,6 +110,93 @@ public function addChildByProductSku($sku, $optionId, \Magento\Bundle\Api\Data\L * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ + public function saveChild( + $sku, + \Magento\Bundle\Api\Data\LinkInterface $linkedProduct + ) { + $product = $this->productRepository->get($sku); + if ($product->getTypeId() != \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) { + throw new InputException( + __('Product with specified sku: "%1" is not a bundle product', [$product->getSku()]) + ); + } + + /** @var \Magento\Catalog\Model\Product $linkProductModel */ + $linkProductModel = $this->productRepository->get($linkedProduct->getSku()); + if ($linkProductModel->isComposite()) { + throw new InputException(__('Bundle product could not contain another composite product')); + } + + if (!$linkedProduct->getId()) { + throw new InputException(__('Id field of product link is required')); + } + + /** @var \Magento\Bundle\Model\Selection $selectionModel */ + $selectionModel = $this->bundleSelection->create(); + $selectionModel->load($linkedProduct->getId()); + if (!$selectionModel->getId()) { + throw new InputException(__('Can not find product link with id "%1"', [$linkedProduct->getId()])); + } + + $selectionModel = $this->mapProductLinkToSelectionModel( + $selectionModel, + $linkedProduct, + $linkProductModel->getId(), + $product->getId() + ); + + try { + $selectionModel->save(); + } catch (\Exception $e) { + throw new CouldNotSaveException(__('Could not save child: "%1"', $e->getMessage()), $e); + } + + return true; + } + + /** + * @param \Magento\Bundle\Model\Selection $selectionModel + * @param \Magento\Bundle\Api\Data\LinkInterface $productLink + * @param string $linkedProductId + * @param string $parentProductId + * @return \Magento\Bundle\Model\Selection + */ + protected function mapProductLinkToSelectionModel( + \Magento\Bundle\Model\Selection $selectionModel, + \Magento\Bundle\Api\Data\LinkInterface $productLink, + $linkedProductId, + $parentProductId + ) { + $selectionModel->setProductId($linkedProductId); + $selectionModel->setParentProductId($parentProductId); + if (($productLink->getOptionId() !== null)) { + $selectionModel->setOptionId($productLink->getOptionId()); + } + if ($productLink->getPosition() !== null) { + $selectionModel->setPosition($productLink->getPosition()); + } + if ($productLink->getQty() !== null) { + $selectionModel->setSelectionQty($productLink->getQty()); + } + if ($productLink->getPriceType() !== null) { + $selectionModel->setSelectionPriceType($productLink->getPriceType()); + } + if ($productLink->getPrice() !== null) { + $selectionModel->setSelectionPriceValue($productLink->getPrice()); + } + if ($productLink->getCanChangeQuantity() !== null) { + $selectionModel->setSelectionCanChangeQty($productLink->getCanChangeQuantity()); + } + if ($productLink->getIsDefault() !== null) { + $selectionModel->setIsDefault($productLink->getIsDefault()); + } + + return $selectionModel; + } + + /** + * {@inheritdoc} + */ public function addChild( \Magento\Catalog\Api\Data\ProductInterface $product, $optionId, @@ -119,17 +209,10 @@ public function addChild( } $options = $this->optionCollection->create(); - $options->setProductIdFilter($product->getId())->joinValues($this->storeManager->getStore()->getId()); - $isNewOption = true; - /** @var \Magento\Bundle\Model\Option $option */ - foreach ($options as $option) { - if ($option->getOptionId() == $optionId) { - $isNewOption = false; - break; - } - } + $options->setIdFilter($optionId); + $existingOption = $options->getFirstItem(); - if ($isNewOption) { + if (!$existingOption->getId()) { throw new InputException( __( 'Product with specified sku: "%1" does not contain option: "%2"', @@ -161,16 +244,13 @@ public function addChild( } $selectionModel = $this->bundleSelection->create(); - $selectionModel->setOptionId($optionId) - ->setPosition($linkedProduct->getPosition()) - ->setSelectionQty($linkedProduct->getQty()) - ->setSelectionPriceType($linkedProduct->getPriceType()) - ->setSelectionPriceValue($linkedProduct->getPrice()) - ->setSelectionCanChangeQty($linkedProduct->getCanChangeQuantity()) - ->setProductId($linkProductModel->getId()) - ->setParentProductId($product->getId()) - ->setIsDefault($linkedProduct->getIsDefault()) - ->setWebsiteId($this->storeManager->getStore()->getWebsiteId()); + $selectionModel = $this->mapProductLinkToSelectionModel( + $selectionModel, + $linkedProduct, + $linkProductModel->getId(), + $product->getId() + ); + $selectionModel->setOptionId($optionId); try { $selectionModel->save(); @@ -242,8 +322,9 @@ private function buildLink(\Magento\Catalog\Model\Product $selection, \Magento\C '\Magento\Bundle\Api\Data\LinkInterface' ); $link->setIsDefault($selection->getIsDefault()) + ->setId($selection->getSelectionId()) ->setQty($selection->getSelectionQty()) - ->setIsDefined($selection->getSelectionCanChangeQty()) + ->setCanChangeQuantity($selection->getSelectionCanChangeQty()) ->setPrice($selectionPrice) ->setPriceType($selectionPriceType); return $link; @@ -251,7 +332,7 @@ private function buildLink(\Magento\Catalog\Model\Product $selection, \Magento\C /** * @param \Magento\Catalog\Api\Data\ProductInterface $product - * @return \Magento\Bundle\Api\Data\OptionTypeInterface[] + * @return \Magento\Bundle\Api\Data\OptionInterface[] */ private function getOptions(\Magento\Catalog\Api\Data\ProductInterface $product) { diff --git a/app/code/Magento/Bundle/Model/OptionRepository.php b/app/code/Magento/Bundle/Model/OptionRepository.php index 024b6f1cd2482..78caa83e801b3 100644 --- a/app/code/Magento/Bundle/Model/OptionRepository.php +++ b/app/code/Magento/Bundle/Model/OptionRepository.php @@ -161,7 +161,6 @@ public function deleteById($sku, $optionId) /** * {@inheritdoc} - * @SuppressWarnings(PHPMD.NPathComplexity) */ public function save( \Magento\Catalog\Api\Data\ProductInterface $product, @@ -170,35 +169,25 @@ public function save( $option->setStoreId($this->storeManager->getStore()->getId()); $option->setParentId($product->getId()); - if (!$option->getOptionId()) { + $optionId = $option->getOptionId(); + $linksToAdd = []; + if (!$optionId) { $option->setDefaultTitle($option->getTitle()); - $linksToAdd = is_array($option->getProductLinks()) ? $option->getProductLinks() : []; + if (is_array($option->getProductLinks())) { + $linksToAdd = $option->getProductLinks(); + } } else { $optionCollection = $this->type->getOptionsCollection($product); - $optionCollection->setIdFilter($option->getOptionId()); /** @var \Magento\Bundle\Model\Option $existingOption */ - $existingOption = $optionCollection->getFirstItem(); + $existingOption = $optionCollection->getItemById($option->getOptionId()); - if (!$existingOption->getOptionId()) { + if (!isset($existingOption) || !$existingOption->getOptionId()) { throw new NoSuchEntityException(__('Requested option doesn\'t exist')); } $option->setData(array_merge($existingOption->getData(), $option->getData())); - - /** @var \Magento\Bundle\Api\Data\LinkInterface[] $existingLinks */ - $existingLinks = is_array($existingOption->getProductLinks()) ? $existingOption->getProductLinks() : []; - - /** @var \Magento\Bundle\Api\Data\LinkInterface[] $newProductLinks */ - $newProductLinks = is_array($option->getProductLinks()) ? $option->getProductLinks() : []; - - /** @var \Magento\Bundle\Api\Data\LinkInterface[] $linksToDelete */ - $linksToDelete = array_udiff($existingLinks, $newProductLinks, [$this, 'compareLinks']); - foreach ($linksToDelete as $link) { - $this->linkManagement->removeChild($product->getSku(), $option->getOptionId(), $link->getSku()); - } - /** @var \Magento\Bundle\Api\Data\LinkInterface[] $linksToAdd */ - $linksToAdd = array_udiff($newProductLinks, $existingLinks, [$this, 'compareLinks']); + $this->updateOptionSelection($product, $option); } try { @@ -215,6 +204,50 @@ public function save( return $option->getOptionId(); } + /** + * Update option selections + * + * @param \Magento\Catalog\Api\Data\ProductInterface $product + * @param \Magento\Bundle\Api\Data\OptionInterface $option + * @return $this + */ + protected function updateOptionSelection( + \Magento\Catalog\Api\Data\ProductInterface $product, + \Magento\Bundle\Api\Data\OptionInterface $option + ) { + $optionId = $option->getOptionId(); + $existingLinks = $this->linkManagement->getChildren($product->getSku(), $optionId); + $linksToAdd = []; + $linksToUpdate = []; + $linksToDelete = []; + if (is_array($option->getProductLinks())) { + $productLinks = $option->getProductLinks(); + foreach ($productLinks as $productLink) { + if (!$productLink->getId()) { + $linksToAdd[] = $productLink; + } else { + $linksToUpdate[] = $productLink; + } + } + /** @var \Magento\Bundle\Api\Data\LinkInterface[] $linksToDelete */ + $linksToDelete = array_udiff($existingLinks, $linksToUpdate, [$this, 'compareLinks']); + } + foreach ($linksToUpdate as $linkedProduct) { + $this->linkManagement->saveChild($product->getSku(), $linkedProduct); + } + foreach ($linksToDelete as $linkedProduct) { + $this->linkManagement->removeChild( + $product->getSku(), + $option->getOptionId(), + $linkedProduct->getSku() + ); + } + foreach ($linksToAdd as $linkedProduct) { + $this->linkManagement->addChild($product, $option->getOptionId(), $linkedProduct); + } + return $this; + } + /** * @param string $sku * @return \Magento\Catalog\Api\Data\ProductInterface @@ -241,7 +274,7 @@ private function compareLinks( \Magento\Bundle\Api\Data\LinkInterface $firstLink, \Magento\Bundle\Api\Data\LinkInterface $secondLink ) { - if ($firstLink->getSku() == $secondLink->getSku()) { + if ($firstLink->getId() == $secondLink->getId()) { return 0; } else { return 1; diff --git a/app/code/Magento/Bundle/Model/Plugin/BundleLoadOptions.php b/app/code/Magento/Bundle/Model/Plugin/BundleLoadOptions.php index 76b5dd84b3620..16bce777104a2 100644 --- a/app/code/Magento/Bundle/Model/Plugin/BundleLoadOptions.php +++ b/app/code/Magento/Bundle/Model/Plugin/BundleLoadOptions.php @@ -52,7 +52,10 @@ public function aroundLoad( return $product; } - $productExtension = $this->productExtensionFactory->create(); + $productExtension = $product->getExtensionAttributes(); + if ($productExtension === null) { + $productExtension = $this->productExtensionFactory->create(); + } $productExtension->setBundleProductOptions($this->productOptionList->getItems($product)); $product->setExtensionAttributes($productExtension); diff --git a/app/code/Magento/Bundle/Model/Plugin/BundleSaveOptions.php b/app/code/Magento/Bundle/Model/Plugin/BundleSaveOptions.php index b0e9ee6de4700..ccfd78d105480 100644 --- a/app/code/Magento/Bundle/Model/Plugin/BundleSaveOptions.php +++ b/app/code/Magento/Bundle/Model/Plugin/BundleSaveOptions.php @@ -17,8 +17,9 @@ class BundleSaveOptions /** * @param \Magento\Bundle\Api\ProductOptionRepositoryInterface $optionRepository */ - public function __construct(\Magento\Bundle\Api\ProductOptionRepositoryInterface $optionRepository) - { + public function __construct( + \Magento\Bundle\Api\ProductOptionRepositoryInterface $optionRepository + ) { $this->optionRepository = $optionRepository; } @@ -45,13 +46,37 @@ public function aroundSave( } /* @var \Magento\Bundle\Api\Data\OptionInterface[] $options */ - $bundleProductOptions = $product->getExtensionAttributes()->getBundleProductOptions(); + $extendedAttributes = $product->getExtensionAttributes(); + if ($extendedAttributes === null) { + return $result; + } + $bundleProductOptions = $extendedAttributes->getBundleProductOptions(); + if ($bundleProductOptions == null) { + return $result; + } - if (is_array($bundleProductOptions)) { - foreach ($bundleProductOptions as $option) { - $this->optionRepository->save($result, $option); + /** @var \Magento\Bundle\Api\Data\OptionInterface[] $bundleProductOptions */ + $existingOptions = $this->optionRepository->getList($product->getSku()); + $existingOptionsMap = []; + foreach ($existingOptions as $existingOption) { + $existingOptionsMap[$existingOption->getOptionId()] = $existingOption; + } + $updatedOptionIds = []; + foreach ($bundleProductOptions as $bundleOption) { + $optionId = $bundleOption->getOptionId(); + if ($optionId) { + $updatedOptionIds[] = $optionId; } } - return $result; + $optionIdsToDelete = array_diff(array_keys($existingOptionsMap), $updatedOptionIds); + //Handle new and existing options + foreach ($bundleProductOptions as $option) { + $this->optionRepository->save($result, $option); + } + //Delete options that are not in the list + foreach ($optionIdsToDelete as $optionId) { + $this->optionRepository->delete($existingOptionsMap[$optionId]); + } + return $subject->get($result->getSku(), false, $result->getStoreId(), true); } } diff --git a/app/code/Magento/Bundle/Model/Product/LinksList.php b/app/code/Magento/Bundle/Model/Product/LinksList.php index d0b6a78635b6a..2d0076f67847b 100644 --- a/app/code/Magento/Bundle/Model/Product/LinksList.php +++ b/app/code/Magento/Bundle/Model/Product/LinksList.php @@ -61,8 +61,9 @@ public function getItems(\Magento\Catalog\Api\Data\ProductInterface $product, $o '\Magento\Bundle\Api\Data\LinkInterface' ); $productLink->setIsDefault($selection->getIsDefault()) + ->setId($selection->getSelectionId()) ->setQty($selection->getSelectionQty()) - ->setIsDefined($selection->getSelectionCanChangeQty()) + ->setCanChangeQuantity($selection->getSelectionCanChangeQty()) ->setPrice($selectionPrice) ->setPriceType($selectionPriceType); $productLinks[] = $productLink; diff --git a/app/code/Magento/Bundle/Model/Selection.php b/app/code/Magento/Bundle/Model/Selection.php index 919c54051566f..1f68f9795dd2b 100644 --- a/app/code/Magento/Bundle/Model/Selection.php +++ b/app/code/Magento/Bundle/Model/Selection.php @@ -8,6 +8,8 @@ /** * Bundle Selection Model * + * @method int getSelectionId() + * @method \Magento\Bundle\Model\Selection setSelectionId(int $value) * @method int getOptionId() * @method \Magento\Bundle\Model\Selection setOptionId(int $value) * @method int getParentProductId() diff --git a/app/code/Magento/Bundle/Model/Source/Option/Selection/Price/Type.php b/app/code/Magento/Bundle/Model/Source/Option/Selection/Price/Type.php index 706de8be46be2..a541ac48c1248 100644 --- a/app/code/Magento/Bundle/Model/Source/Option/Selection/Price/Type.php +++ b/app/code/Magento/Bundle/Model/Source/Option/Selection/Price/Type.php @@ -5,6 +5,8 @@ */ namespace Magento\Bundle\Model\Source\Option\Selection\Price; +use Magento\Bundle\Api\Data\LinkInterface; + /** * Extended Attributes Source Model * @@ -17,6 +19,9 @@ class Type implements \Magento\Framework\Option\ArrayInterface */ public function toOptionArray() { - return [['value' => '0', 'label' => __('Fixed')], ['value' => '1', 'label' => __('Percent')]]; + return [ + ['value' => LinkInterface::PRICE_TYPE_FIXED, 'label' => __('Fixed')], + ['value' => LinkInterface::PRICE_TYPE_PERCENT, 'label' => __('Percent')] + ]; } } diff --git a/app/code/Magento/Bundle/Test/Unit/Model/LinkManagementTest.php b/app/code/Magento/Bundle/Test/Unit/Model/LinkManagementTest.php index a1d9391b3e112..e4f49d48adcf3 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/LinkManagementTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/LinkManagementTest.php @@ -111,7 +111,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->option = $this->getMockBuilder('Magento\Bundle\Model\Option') - ->setMethods(['getSelections', '__wakeup']) + ->setMethods(['getSelections', 'getOptionId', '__wakeup']) ->disableOriginalConstructor() ->getMock(); $this->optionCollection = $this->getMockBuilder('Magento\Bundle\Model\Resource\Option\Collection') @@ -193,14 +193,48 @@ public function testGetChildren() ->willReturnSelf(); $this->link->expects($this->once())->method('setIsDefault')->willReturnSelf(); $this->link->expects($this->once())->method('setQty')->willReturnSelf(); - $this->link->expects($this->once())->method('setIsDefined')->willReturnSelf(); + $this->link->expects($this->once())->method('setCanChangeQuantity')->willReturnSelf(); $this->link->expects($this->once())->method('setPrice')->willReturnSelf(); $this->link->expects($this->once())->method('setPriceType')->willReturnSelf(); + $this->link->expects($this->once())->method('setId')->willReturnSelf(); $this->linkFactory->expects($this->once())->method('create')->willReturn($this->link); $this->assertEquals([$this->link], $this->model->getChildren($productSku)); } + public function testGetChildrenWithOptionId() + { + $productSku = 'productSku'; + + $this->getOptions(); + + $this->productRepository->expects($this->any())->method('get')->with($this->equalTo($productSku)) + ->will($this->returnValue($this->product)); + + $this->product->expects($this->once())->method('getTypeId')->will($this->returnValue('bundle')); + + $this->productType->expects($this->once())->method('setStoreFilter')->with( + $this->equalTo($this->storeId), + $this->product + ); + $this->productType->expects($this->once())->method('getSelectionsCollection') + ->with($this->equalTo($this->optionIds), $this->equalTo($this->product)) + ->will($this->returnValue($this->selectionCollection)); + $this->productType->expects($this->once())->method('getOptionsIds')->with($this->equalTo($this->product)) + ->will($this->returnValue($this->optionIds)); + + $this->optionCollection->expects($this->once())->method('appendSelections') + ->with($this->equalTo($this->selectionCollection)) + ->will($this->returnValue([$this->option])); + + $this->option->expects($this->any())->method('getOptionId')->will($this->returnValue(10)); + $this->option->expects($this->never())->method('getSelections'); + + $this->dataObjectHelperMock->expects($this->never())->method('populateWithArray'); + + $this->assertEquals([], $this->model->getChildren($productSku, 1)); + } + /** * @expectedException \Magento\Framework\Exception\InputException */ @@ -243,31 +277,29 @@ public function testAddChildNonExistingOption() $productMock->expects($this->once())->method('getTypeId')->will($this->returnValue( \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE )); - $productMock->expects($this->once())->method('getId')->will($this->returnValue('product_id')); $store = $this->getMock('\Magento\Store\Model\Store', [], [], '', false); $this->storeManagerMock->expects($this->any())->method('getStore')->will($this->returnValue($store)); $store->expects($this->any())->method('getId')->will($this->returnValue(0)); - $option = $this->getMockBuilder('\Magento\Bundle\Model\Option')->disableOriginalConstructor() - ->setMethods(['getOptionId', '__wakeup']) + $emptyOption = $this->getMockBuilder('\Magento\Bundle\Model\Option')->disableOriginalConstructor() + ->setMethods(['getId', '__wakeup']) ->getMock(); - $option->expects($this->once())->method('getOptionId')->will($this->returnValue(2)); + $emptyOption->expects($this->once()) + ->method('getId') + ->will($this->returnValue(null)); $optionsCollectionMock = $this->getMock( '\Magento\Bundle\Model\Resource\Option\Collection', [], [], '', false ); $optionsCollectionMock->expects($this->once()) - ->method('setProductIdFilter') - ->with($this->equalTo('product_id')) + ->method('setIdFilter') + ->with($this->equalTo(1)) ->will($this->returnSelf()); $optionsCollectionMock->expects($this->once()) - ->method('joinValues') - ->with($this->equalTo(0)) - ->will($this->returnSelf()); - $optionsCollectionMock->expects($this->any())->method('getIterator')->will( - $this->returnValue(new \ArrayIterator([$option])) - ); + ->method('getFirstItem') + ->will($this->returnValue($emptyOption)); + $this->optionCollectionFactoryMock->expects($this->any())->method('create')->will( $this->returnValue($optionsCollectionMock) ); @@ -304,22 +336,18 @@ public function testAddChildLinkedProductIsComposite() $store->expects($this->any())->method('getId')->will($this->returnValue(0)); $option = $this->getMockBuilder('\Magento\Bundle\Model\Option')->disableOriginalConstructor() - ->setMethods(['getOptionId', '__wakeup']) + ->setMethods(['getId', '__wakeup']) ->getMock(); - $option->expects($this->once())->method('getOptionId')->will($this->returnValue(1)); + $option->expects($this->once())->method('getId')->will($this->returnValue(1)); $optionsCollectionMock = $this->getMock('\Magento\Bundle\Model\Resource\Option\Collection', [], [], '', false); $optionsCollectionMock->expects($this->once()) - ->method('setProductIdFilter') - ->with($this->equalTo('product_id')) + ->method('setIdFilter') + ->with($this->equalTo('1')) ->will($this->returnSelf()); $optionsCollectionMock->expects($this->once()) - ->method('joinValues') - ->with($this->equalTo(0)) - ->will($this->returnSelf()); - $optionsCollectionMock->expects($this->any())->method('getIterator')->will( - $this->returnValue(new \ArrayIterator([$option])) - ); + ->method('getFirstItem') + ->will($this->returnValue($option)); $this->optionCollectionFactoryMock->expects($this->any())->method('create')->will( $this->returnValue($optionsCollectionMock) ); @@ -359,22 +387,18 @@ public function testAddChildProductAlreadyExistsInOption() $store->expects($this->any())->method('getId')->will($this->returnValue(0)); $option = $this->getMockBuilder('\Magento\Bundle\Model\Option')->disableOriginalConstructor() - ->setMethods(['getOptionId', '__wakeup']) + ->setMethods(['getId', '__wakeup']) ->getMock(); - $option->expects($this->once())->method('getOptionId')->will($this->returnValue(1)); + $option->expects($this->once())->method('getId')->will($this->returnValue(1)); $optionsCollectionMock = $this->getMock('\Magento\Bundle\Model\Resource\Option\Collection', [], [], '', false); $optionsCollectionMock->expects($this->once()) - ->method('setProductIdFilter') - ->with($this->equalTo('product_id')) + ->method('setIdFilter') + ->with($this->equalTo(1)) ->will($this->returnSelf()); $optionsCollectionMock->expects($this->once()) - ->method('joinValues') - ->with($this->equalTo(0)) - ->will($this->returnSelf()); - $optionsCollectionMock->expects($this->any())->method('getIterator')->will( - $this->returnValue(new \ArrayIterator([$option])) - ); + ->method('getFirstItem') + ->will($this->returnValue($option)); $this->optionCollectionFactoryMock->expects($this->any())->method('create')->will( $this->returnValue($optionsCollectionMock) ); @@ -420,24 +444,20 @@ public function testAddChildCouldNotSave() $store->expects($this->any())->method('getId')->will($this->returnValue(0)); $option = $this->getMockBuilder('\Magento\Bundle\Model\Option')->disableOriginalConstructor() - ->setMethods(['getOptionId', '__wakeup']) + ->setMethods(['getId', '__wakeup']) ->getMock(); - $option->expects($this->once())->method('getOptionId')->will($this->returnValue(1)); + $option->expects($this->once())->method('getId')->will($this->returnValue(1)); $optionsCollectionMock = $this->getMock( '\Magento\Bundle\Model\Resource\Option\Collection', [], [], '', false ); $optionsCollectionMock->expects($this->once()) - ->method('setProductIdFilter') - ->with($this->equalTo('product_id')) + ->method('setIdFilter') + ->with($this->equalTo(1)) ->will($this->returnSelf()); $optionsCollectionMock->expects($this->once()) - ->method('joinValues') - ->with($this->equalTo(0)) - ->will($this->returnSelf()); - $optionsCollectionMock->expects($this->any())->method('getIterator')->will( - $this->returnValue(new \ArrayIterator([$option])) - ); + ->method('getFirstItem') + ->will($this->returnValue($option)); $this->optionCollectionFactoryMock->expects($this->any())->method('create')->will( $this->returnValue($optionsCollectionMock) ); @@ -452,7 +472,7 @@ public function testAddChildCouldNotSave() ->will($this->returnValue($selections)); $this->bundleFactoryMock->expects($this->once())->method('create')->will($this->returnValue($bundle)); - $selection = $this->getMock('\Magento\Framework\Object', ['save'], [], '', false); + $selection = $this->getMock('\Magento\Bundle\Model\Selection', ['save'], [], '', false); $selection->expects($this->once())->method('save') ->will( $this->returnCallback( @@ -491,24 +511,20 @@ public function testAddChild() $store->expects($this->any())->method('getId')->will($this->returnValue(0)); $option = $this->getMockBuilder('\Magento\Bundle\Model\Option')->disableOriginalConstructor() - ->setMethods(['getOptionId', '__wakeup']) + ->setMethods(['getId', '__wakeup']) ->getMock(); - $option->expects($this->once())->method('getOptionId')->will($this->returnValue(1)); + $option->expects($this->once())->method('getId')->will($this->returnValue(1)); $optionsCollectionMock = $this->getMock( '\Magento\Bundle\Model\Resource\Option\Collection', [], [], '', false ); $optionsCollectionMock->expects($this->once()) - ->method('setProductIdFilter') - ->with($this->equalTo('product_id')) + ->method('setIdFilter') + ->with($this->equalTo(1)) ->will($this->returnSelf()); $optionsCollectionMock->expects($this->once()) - ->method('joinValues') - ->with($this->equalTo(0)) - ->will($this->returnSelf()); - $optionsCollectionMock->expects($this->any())->method('getIterator')->will( - $this->returnValue(new \ArrayIterator([$option])) - ); + ->method('getFirstItem') + ->will($this->returnValue($option)); $this->optionCollectionFactoryMock->expects($this->any())->method('create')->will( $this->returnValue($optionsCollectionMock) ); @@ -523,7 +539,7 @@ public function testAddChild() ->will($this->returnValue($selections)); $this->bundleFactoryMock->expects($this->once())->method('create')->will($this->returnValue($bundle)); - $selection = $this->getMock('\Magento\Framework\Object', ['save', 'getId'], [], '', false); + $selection = $this->getMock('\Magento\Bundle\Model\Selection', ['save', 'getId'], [], '', false); $selection->expects($this->once())->method('save'); $selection->expects($this->once())->method('getId')->will($this->returnValue(42)); $this->bundleSelectionMock->expects($this->once())->method('create')->will($this->returnValue($selection)); @@ -531,6 +547,298 @@ public function testAddChild() $this->assertEquals(42, $result); } + public function testSaveChild() + { + $id = 12; + $optionId = 1; + $position = 3; + $qty = 2; + $priceType = 1; + $price = 10.5; + $canChangeQuantity = true; + $isDefault = true; + $linkProductId = 45; + $parentProductId = 32; + $bundleProductSku = 'bundleProductSku'; + + $productLink = $this->getMock('\Magento\Bundle\Api\Data\LinkInterface'); + $productLink->expects($this->any())->method('getSku')->will($this->returnValue('linked_product_sku')); + $productLink->expects($this->any())->method('getId')->will($this->returnValue($id)); + $productLink->expects($this->any())->method('getOptionId')->will($this->returnValue($optionId)); + $productLink->expects($this->any())->method('getPosition')->will($this->returnValue($position)); + $productLink->expects($this->any())->method('getQty')->will($this->returnValue($qty)); + $productLink->expects($this->any())->method('getPriceType')->will($this->returnValue($priceType)); + $productLink->expects($this->any())->method('getPrice')->will($this->returnValue($price)); + $productLink->expects($this->any())->method('getCanChangeQuantity')->will($this->returnValue($canChangeQuantity)); + $productLink->expects($this->any())->method('getIsDefault')->will($this->returnValue($isDefault)); + + $productMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); + $productMock->expects($this->once())->method('getTypeId')->will($this->returnValue( + \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE + )); + $productMock->expects($this->any())->method('getId')->will($this->returnValue($parentProductId)); + + $linkedProductMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); + $linkedProductMock->expects($this->any())->method('getId')->will($this->returnValue($linkProductId)); + $linkedProductMock->expects($this->once())->method('isComposite')->will($this->returnValue(false)); + $this->productRepository + ->expects($this->at(0)) + ->method('get') + ->with($bundleProductSku) + ->will($this->returnValue($productMock)); + $this->productRepository + ->expects($this->at(1)) + ->method('get') + ->with('linked_product_sku') + ->will($this->returnValue($linkedProductMock)); + + $store = $this->getMock('\Magento\Store\Model\Store', [], [], '', false); + $this->storeManagerMock->expects($this->any())->method('getStore')->will($this->returnValue($store)); + $store->expects($this->any())->method('getId')->will($this->returnValue(0)); + + $selection = $this->getMock( + '\Magento\Bundle\Model\Selection', + [ + 'save', + 'getId', + 'load', + 'setProductId', + 'setParentProductId', + 'setOptionId', + 'setPosition', + 'setSelectionQty', + 'setSelectionPriceType', + 'setSelectionPriceValue', + 'setSelectionCanChangeQty', + 'setIsDefault' + ], + [], + '', + false + ); + $selection->expects($this->once())->method('save'); + $selection->expects($this->once())->method('load')->with($id)->will($this->returnSelf()); + $selection->expects($this->any())->method('getId')->will($this->returnValue($id)); + $selection->expects($this->once())->method('setProductId')->with($linkProductId); + $selection->expects($this->once())->method('setParentProductId')->with($parentProductId); + $selection->expects($this->once())->method('setOptionId')->with($optionId); + $selection->expects($this->once())->method('setPosition')->with($position); + $selection->expects($this->once())->method('setSelectionQty')->with($qty); + $selection->expects($this->once())->method('setSelectionPriceType')->with($priceType); + $selection->expects($this->once())->method('setSelectionPriceValue')->with($price); + $selection->expects($this->once())->method('setSelectionCanChangeQty')->with($canChangeQuantity); + $selection->expects($this->once())->method('setIsDefault')->with($isDefault); + + $this->bundleSelectionMock->expects($this->once())->method('create')->will($this->returnValue($selection)); + $this->assertTrue($this->model->saveChild($bundleProductSku, $productLink)); + } + + /** + * @expectedException \Magento\Framework\Exception\CouldNotSaveException + */ + public function testSaveChildFailedToSave() + { + $id = 12; + $linkProductId = 45; + $parentProductId = 32; + $productLink = $this->getMock('\Magento\Bundle\Api\Data\LinkInterface'); + $productLink->expects($this->any())->method('getSku')->will($this->returnValue('linked_product_sku')); + $productLink->expects($this->any())->method('getId')->will($this->returnValue($id)); + $bundleProductSku = 'bundleProductSku'; + + $productMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); + $productMock->expects($this->once())->method('getTypeId')->will($this->returnValue( + \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE + )); + $productMock->expects($this->any())->method('getId')->will($this->returnValue($parentProductId)); + + $linkedProductMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); + $linkedProductMock->expects($this->any())->method('getId')->will($this->returnValue($linkProductId)); + $linkedProductMock->expects($this->once())->method('isComposite')->will($this->returnValue(false)); + $this->productRepository + ->expects($this->at(0)) + ->method('get') + ->with($bundleProductSku) + ->will($this->returnValue($productMock)); + $this->productRepository + ->expects($this->at(1)) + ->method('get') + ->with('linked_product_sku') + ->will($this->returnValue($linkedProductMock)); + + $store = $this->getMock('\Magento\Store\Model\Store', [], [], '', false); + $this->storeManagerMock->expects($this->any())->method('getStore')->will($this->returnValue($store)); + $store->expects($this->any())->method('getId')->will($this->returnValue(0)); + + $selection = $this->getMock( + '\Magento\Bundle\Model\Selection', + [ + 'save', + 'getId', + 'load', + 'setProductId', + 'setParentProductId', + 'setSelectionId', + 'setOptionId', + 'setPosition', + 'setSelectionQty', + 'setSelectionPriceType', + 'setSelectionPriceValue', + 'setSelectionCanChangeQty', + 'setIsDefault' + ], + [], + '', + false + ); + $mockException = $this->getMock('\Exception'); + $selection->expects($this->once())->method('save')->will($this->throwException($mockException)); + $selection->expects($this->once())->method('load')->with($id)->will($this->returnSelf()); + $selection->expects($this->any())->method('getId')->will($this->returnValue($id)); + $selection->expects($this->once())->method('setProductId')->with($linkProductId); + + $this->bundleSelectionMock->expects($this->once())->method('create')->will($this->returnValue($selection)); + $this->model->saveChild($bundleProductSku, $productLink); + } + + /** + * @expectedException \Magento\Framework\Exception\InputException + */ + public function testSaveChildWithoutId() + { + $bundleProductSku = "bundleSku"; + $linkedProductSku = 'simple'; + $productLink = $this->getMock('\Magento\Bundle\Api\Data\LinkInterface'); + $productLink->expects($this->any())->method('getId')->will($this->returnValue(null)); + $productLink->expects($this->any())->method('getSku')->will($this->returnValue($linkedProductSku)); + + $productMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); + $productMock->expects($this->once())->method('getTypeId')->will($this->returnValue( + \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE + )); + + $linkedProductMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); + $linkedProductMock->expects($this->once())->method('isComposite')->will($this->returnValue(false)); + $this->productRepository + ->expects($this->at(0)) + ->method('get') + ->with($bundleProductSku) + ->will($this->returnValue($productMock)); + $this->productRepository + ->expects($this->at(1)) + ->method('get') + ->with($linkedProductSku) + ->will($this->returnValue($linkedProductMock)); + + $this->model->saveChild($bundleProductSku, $productLink); + } + + /** + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage Can not find product link with id "12345" + */ + public function testSaveChildWithInvalidId() + { + $id = 12345; + $linkedProductSku = 'simple'; + $bundleProductSku = "bundleProductSku"; + $productLink = $this->getMock('\Magento\Bundle\Api\Data\LinkInterface'); + $productLink->expects($this->any())->method('getId')->will($this->returnValue($id)); + $productLink->expects($this->any())->method('getSku')->will($this->returnValue($linkedProductSku)); + + $productMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); + $productMock->expects($this->once())->method('getTypeId')->will($this->returnValue( + \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE + )); + + $linkedProductMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); + $linkedProductMock->expects($this->once())->method('isComposite')->will($this->returnValue(false)); + $this->productRepository + ->expects($this->at(0)) + ->method('get') + ->with($bundleProductSku) + ->will($this->returnValue($productMock)); + $this->productRepository + ->expects($this->at(1)) + ->method('get') + ->with($linkedProductSku) + ->will($this->returnValue($linkedProductMock)); + + $selection = $this->getMock( + '\Magento\Bundle\Model\Selection', + [ + 'getId', + 'load', + ], + [], + '', + false + ); + $selection->expects($this->once())->method('load')->with($id)->will($this->returnSelf()); + $selection->expects($this->any())->method('getId')->will($this->returnValue(null)); + + $this->bundleSelectionMock->expects($this->once())->method('create')->will($this->returnValue($selection)); + + $this->model->saveChild($bundleProductSku, $productLink); + } + + /** + * @expectedException \Magento\Framework\Exception\InputException + */ + public function testSaveChildWithCompositeProductLink() + { + $bundleProductSku = "bundleProductSku"; + $id = 12; + $linkedProductSku = 'simple'; + $productLink = $this->getMock('\Magento\Bundle\Api\Data\LinkInterface'); + $productLink->expects($this->any())->method('getId')->will($this->returnValue($id)); + $productLink->expects($this->any())->method('getSku')->will($this->returnValue($linkedProductSku)); + + $productMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); + $productMock->expects($this->once())->method('getTypeId')->will($this->returnValue( + \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE + )); + + $linkedProductMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); + $linkedProductMock->expects($this->once())->method('isComposite')->will($this->returnValue(true)); + $this->productRepository + ->expects($this->at(0)) + ->method('get') + ->with($bundleProductSku) + ->will($this->returnValue($productMock)); + $this->productRepository + ->expects($this->at(1)) + ->method('get') + ->with($linkedProductSku) + ->will($this->returnValue($linkedProductMock)); + + $this->model->saveChild($bundleProductSku, $productLink); + } + + /** + * @expectedException \Magento\Framework\Exception\InputException + */ + public function testSaveChildWithSimpleProduct() + { + $id = 12; + $linkedProductSku = 'simple'; + $bundleProductSku = "bundleProductSku"; + + $productLink = $this->getMock('\Magento\Bundle\Api\Data\LinkInterface'); + $productLink->expects($this->any())->method('getId')->will($this->returnValue($id)); + $productLink->expects($this->any())->method('getSku')->will($this->returnValue($linkedProductSku)); + + $productMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); + $productMock->expects($this->once())->method('getTypeId')->will($this->returnValue( + \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE + )); + + $this->productRepository->expects($this->once())->method('get')->with($bundleProductSku) + ->willReturn($productMock); + + $this->model->saveChild($bundleProductSku, $productLink); + } + public function testRemoveChild() { $this->productRepository->expects($this->any())->method('get')->will($this->returnValue($this->product)); diff --git a/app/code/Magento/Bundle/Test/Unit/Model/OptionRepositoryTest.php b/app/code/Magento/Bundle/Test/Unit/Model/OptionRepositoryTest.php index 7b91140a3bfe6..6c4fc418f0390 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/OptionRepositoryTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/OptionRepositoryTest.php @@ -335,8 +335,9 @@ public function testUpdateIfOptionDoesNotExist() ->willReturn($optCollectionMock); $existingOptionMock = $this->getMock('\Magento\Bundle\Model\Option', ['getOptionId'], [], '', false); - $optCollectionMock->expects($this->once())->method('setIdFilter')->with($optionId)->willReturnSelf(); - $optCollectionMock->expects($this->once())->method('getFirstItem')->willReturn($existingOptionMock); + $optCollectionMock->expects($this->once())->method('getItemById') + ->with($optionId) + ->willReturn($existingOptionMock); $existingOptionMock->expects($this->once())->method('getOptionId')->willReturn(null); $this->assertEquals($optionId, $this->model->save($productMock, $optionMock)); @@ -345,13 +346,16 @@ public function testUpdateIfOptionDoesNotExist() /** * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - public function testUpdate() + public function testSaveExistingOption() { $productId = 1; + $productSku = 'bundle_sku'; $storeId = 2; $optionId = 5; $existingOptionId = 5; - $existingOptionTitle = 'option_title'; + $existingLinkToUpdateId = '23'; + $existingLinkToDeleteId = '24'; + $productSkuToDelete = 'simple2'; $storeMock = $this->getMock('\Magento\Store\Model\Store', ['getId'], [], '', false); $storeMock->expects($this->once())->method('getId')->willReturn($storeId); @@ -359,6 +363,7 @@ public function testUpdate() $productMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); $productMock->expects($this->once())->method('getId')->willReturn($productId); + $productMock->expects($this->any())->method('getSku')->willReturn($productSku); $optionMock = $this->getMock( '\Magento\Bundle\Model\Option', [ @@ -378,6 +383,17 @@ public function testUpdate() $optionMock->expects($this->once())->method('setParentId')->with($productId)->willReturnSelf(); $optionMock->expects($this->any())->method('getOptionId')->willReturn($optionId); + $existingLinkToUpdate = $this->getMock('\Magento\Bundle\Api\Data\LinkInterface'); + $existingLinkToUpdate->expects($this->any())->method('getId')->willReturn($existingLinkToUpdateId); + $existingLinkToDelete = $this->getMock('\Magento\Bundle\Api\Data\LinkInterface'); + $existingLinkToDelete->expects($this->any())->method('getId')->willReturn($existingLinkToDeleteId); + $existingLinkToDelete->expects($this->once())->method('getSku')->willReturn($productSkuToDelete); + $existingLinks = [$existingLinkToUpdate, $existingLinkToDelete]; + $this->linkManagementMock->expects($this->once()) + ->method('getChildren') + ->with($productSku, $optionId) + ->willReturn($existingLinks); + $optCollectionMock = $this->getMock('\Magento\Bundle\Model\Resource\Option\Collection', [], [], '', false); $this->typeMock->expects($this->once()) ->method('getOptionsCollection') @@ -390,22 +406,183 @@ public function testUpdate() '', false ); - $optCollectionMock->expects($this->once())->method('setIdFilter')->with($optionId)->willReturnSelf(); - $optCollectionMock->expects($this->once())->method('getFirstItem')->willReturn($existingOptionMock); + $optCollectionMock->expects($this->once())->method('getItemById') + ->with($optionId) + ->willReturn($existingOptionMock); $existingOptionMock->expects($this->any())->method('getOptionId')->willReturn($existingOptionId); - $existingOptionMock->expects($this->once())->method('getProductLinks')->willReturn(null); - $linkedProductMock = $this->getMock('\Magento\Bundle\Api\Data\LinkInterface'); - $optionMock->expects($this->exactly(2))->method('getProductLinks')->willReturn([$linkedProductMock]); + $productLinkUpdate = $this->getMock('\Magento\Bundle\Api\Data\LinkInterface'); + $productLinkUpdate->expects($this->any())->method('getId')->willReturn($existingLinkToUpdateId); + $productLinkNew = $this->getMock('\Magento\Bundle\Api\Data\LinkInterface'); + $productLinkNew->expects($this->any())->method('getId')->willReturn(null); + $optionMock->expects($this->exactly(2)) + ->method('getProductLinks') + ->willReturn([$productLinkUpdate, $productLinkNew]); $this->optionResourceMock->expects($this->once())->method('save')->with($optionMock)->willReturnSelf(); $this->linkManagementMock->expects($this->once()) ->method('addChild') - ->with($productMock, $optionId, $linkedProductMock) - ->willReturn(1); + ->with($productMock, $optionId, $productLinkNew); + $this->linkManagementMock->expects($this->once()) + ->method('saveChild') + ->with($productSku, $productLinkUpdate); + $this->linkManagementMock->expects($this->once()) + ->method('removeChild') + ->with($productSku, $optionId, $productSkuToDelete); + $this->assertEquals($optionId, $this->model->save($productMock, $optionMock)); + } + + /** + * @expectedException \Magento\Framework\Exception\NoSuchEntityException + * @expectedExceptionMessage Requested option doesn't exist + */ + public function testSaveExistingOptionNoSuchOption() + { + $productId = 1; + $productSku = 'bundle_sku'; + $storeId = 2; + $optionId = 5; + + $storeMock = $this->getMock('\Magento\Store\Model\Store', ['getId'], [], '', false); + $storeMock->expects($this->once())->method('getId')->willReturn($storeId); + $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($storeMock); + + $productMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); + $productMock->expects($this->once())->method('getId')->willReturn($productId); + $productMock->expects($this->any())->method('getSku')->willReturn($productSku); + $optionMock = $this->getMock( + '\Magento\Bundle\Model\Option', + [ + 'setStoreId', + 'setParentId', + 'getOptionId', + ], + [], + '', + false + ); + $optionMock->expects($this->once())->method('setStoreId')->with($storeId)->willReturnSelf(); + $optionMock->expects($this->once())->method('setParentId')->with($productId)->willReturnSelf(); + $optionMock->expects($this->any())->method('getOptionId')->willReturn($optionId); + + $optCollectionMock = $this->getMock('\Magento\Bundle\Model\Resource\Option\Collection', [], [], '', false); + $this->typeMock->expects($this->once()) + ->method('getOptionsCollection') + ->with($productMock) + ->willReturn($optCollectionMock); + $existingOptionMock = $this->getMock( + '\Magento\Bundle\Model\Option', + ['getOptionId', 'getTitle', 'getProductLinks'], + [], + '', + false + ); + $optCollectionMock->expects($this->once())->method('getItemById') + ->with($optionId) + ->willReturn($existingOptionMock); + $existingOptionMock->expects($this->any())->method('getOptionId')->willReturn(null); + + $this->model->save($productMock, $optionMock); + } + + public function testSaveNewOption() + { + $productId = 1; + $productSku = 'bundle_sku'; + $storeId = 2; + $optionId = 5; + + $storeMock = $this->getMock('\Magento\Store\Model\Store', ['getId'], [], '', false); + $storeMock->expects($this->once())->method('getId')->willReturn($storeId); + $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($storeMock); + + $productMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); + $productMock->expects($this->once())->method('getId')->willReturn($productId); + $productMock->expects($this->any())->method('getSku')->willReturn($productSku); + $optionMock = $this->getMock( + '\Magento\Bundle\Model\Option', + [ + 'setStoreId', + 'setParentId', + 'getProductLinks', + 'getOptionId', + 'setOptionId', + 'setDefaultTitle', + 'getTitle' + ], + [], + '', + false + ); + $optionMock->expects($this->once())->method('setStoreId')->with($storeId)->willReturnSelf(); + $optionMock->expects($this->once())->method('setParentId')->with($productId)->willReturnSelf(); + $optionMock->method('getOptionId') + ->will($this->onConsecutiveCalls(null, $optionId, $optionId, $optionId, $optionId)); + + $productLink1 = $this->getMock('\Magento\Bundle\Api\Data\LinkInterface'); + $productLink2 = $this->getMock('\Magento\Bundle\Api\Data\LinkInterface'); + $optionMock->expects($this->exactly(2)) + ->method('getProductLinks') + ->willReturn([$productLink1, $productLink2]); + + $this->optionResourceMock->expects($this->once())->method('save')->with($optionMock)->willReturnSelf(); + $this->linkManagementMock->expects($this->at(0)) + ->method('addChild') + ->with($productMock, $optionId, $productLink1); + $this->linkManagementMock->expects($this->at(1)) + ->method('addChild') + ->with($productMock, $optionId, $productLink2); $this->assertEquals($optionId, $this->model->save($productMock, $optionMock)); } + /** + * @expectedException \Magento\Framework\Exception\CouldNotSaveException + * @expectedExceptionMessage Could not save option + */ + public function testSaveCanNotSave() + { + $productId = 1; + $productSku = 'bundle_sku'; + $storeId = 2; + $optionId = 5; + + $storeMock = $this->getMock('\Magento\Store\Model\Store', ['getId'], [], '', false); + $storeMock->expects($this->once())->method('getId')->willReturn($storeId); + $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($storeMock); + + $productMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); + $productMock->expects($this->once())->method('getId')->willReturn($productId); + $productMock->expects($this->any())->method('getSku')->willReturn($productSku); + $optionMock = $this->getMock( + '\Magento\Bundle\Model\Option', + [ + 'setStoreId', + 'setParentId', + 'getProductLinks', + 'getOptionId', + 'setOptionId', + 'setDefaultTitle', + 'getTitle' + ], + [], + '', + false + ); + $optionMock->expects($this->once())->method('setStoreId')->with($storeId)->willReturnSelf(); + $optionMock->expects($this->once())->method('setParentId')->with($productId)->willReturnSelf(); + $optionMock->method('getOptionId')->will($this->onConsecutiveCalls(null, $optionId, $optionId, $optionId)); + + $productLink1 = $this->getMock('\Magento\Bundle\Api\Data\LinkInterface'); + $productLink2 = $this->getMock('\Magento\Bundle\Api\Data\LinkInterface'); + $optionMock->expects($this->exactly(2)) + ->method('getProductLinks') + ->willReturn([$productLink1, $productLink2]); + + $this->optionResourceMock->expects($this->once())->method('save')->with($optionMock) + ->willThrowException($this->getMock('\Exception')); + $this->model->save($productMock, $optionMock); + } + public function testGetList() { $productSku = 'simple'; diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Plugin/BundleLoadOptionsTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Plugin/BundleLoadOptionsTest.php index e4050d99040ca..77d92ff634334 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/Plugin/BundleLoadOptionsTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/Plugin/BundleLoadOptionsTest.php @@ -60,7 +60,6 @@ public function testAroundLoadIfProductTypeNotBundle() public function testAroundLoad() { - $this->markTestSkipped('MAGETWO-34577'); $productMock = $this->getMock( '\Magento\Catalog\Model\Product', ['getTypeId', 'setExtensionAttributes'], @@ -82,6 +81,7 @@ public function testAroundLoad() ->willReturn([$optionMock]); $productExtensionMock = $this->getMockBuilder('\Magento\Catalog\Api\Data\ProductExtension') ->disableOriginalConstructor() + ->setMethods(['setBundleProductOptions', 'getBundleProductOptions']) ->getMock(); $this->productExtensionFactory->expects($this->once()) ->method('create') diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Plugin/BundleSaveOptionsTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Plugin/BundleSaveOptionsTest.php index 1615760e6dbee..62095af664f2b 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/Plugin/BundleSaveOptionsTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/Plugin/BundleSaveOptionsTest.php @@ -43,6 +43,11 @@ class BundleSaveOptionsTest extends \PHPUnit_Framework_TestCase */ protected $productBundleOptionsMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $productInterfaceFactoryMock; + /** * @var \Closure */ @@ -54,7 +59,7 @@ protected function setUp() $this->productOptionRepositoryMock = $this->getMock('Magento\Bundle\Api\ProductOptionRepositoryInterface'); $this->productMock = $this->getMock( 'Magento\Catalog\Model\Product', - ['getExtensionAttributes', 'getTypeId'], + ['getExtensionAttributes', 'getTypeId', 'getSku', 'getStoreId'], [], '', false @@ -62,10 +67,12 @@ protected function setUp() $this->closureMock = function () { return $this->productMock; }; - $this->plugin = new BundleSaveOptions($this->productOptionRepositoryMock); + $this->plugin = new BundleSaveOptions( + $this->productOptionRepositoryMock + ); $this->productExtensionMock = $this->getMock( 'Magento\Catalog\Api\Data\ProductExtension', - ['getBundleProductOptions'], + ['getBundleProductOptions', 'setBundleProductOptions'], [], '', false @@ -98,7 +105,7 @@ public function testAroundSaveWhenProductIsBundleWithoutOptions() ->willReturn($this->productExtensionMock); $this->productExtensionMock->expects($this->once()) ->method('getBundleProductOptions') - ->willReturn([]); + ->willReturn(null); $this->productOptionRepositoryMock->expects($this->never())->method('save'); @@ -110,20 +117,105 @@ public function testAroundSaveWhenProductIsBundleWithoutOptions() public function testAroundSaveWhenProductIsBundleWithOptions() { + $productSku = "bundle_sku"; + $option = $this->getMock('\Magento\Bundle\Api\Data\OptionInterface'); + $this->productMock->expects($this->once())->method('getTypeId')->willReturn('bundle'); + $this->productMock->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->productExtensionMock); + $this->productExtensionMock->expects($this->once()) + ->method('getBundleProductOptions') + ->willReturn([$option]); + + $this->productOptionRepositoryMock->expects($this->once())->method('save')->with($this->productMock, $option); + + $this->productMock->expects($this->exactly(2))->method('getSku') + ->will($this->returnValue($productSku)); + + $this->productOptionRepositoryMock->expects($this->once()) + ->method('getList') + ->with($productSku) + ->will($this->returnValue([])); + + $newProductMock = $this->getMockBuilder('Magento\Catalog\Api\Data\ProductInterface') + ->disableOriginalConstructor()->getMock(); + $this->productRepositoryMock->expects($this->once()) + ->method('get') + ->with($productSku, false, null, true) + ->willReturn($newProductMock); + + $this->assertEquals( + $newProductMock, + $this->plugin->aroundSave($this->productRepositoryMock, $this->closureMock, $this->productMock) + ); + } + + /** + * Test the case where the product has existing options + */ + public function testAroundSaveWhenProductIsBundleWithOptionsAndExistingOptions() + { + $existOption1Id = 10; + $existOption2Id = 11; + $productSku = 'bundle_sku'; + $existingOption1 = $this->getMock('\Magento\Bundle\Api\Data\OptionInterface'); + $existingOption1->expects($this->once()) + ->method('getOptionId') + ->will($this->returnValue($existOption1Id)); + $existingOption2 = $this->getMock('\Magento\Bundle\Api\Data\OptionInterface'); + $existingOption2->expects($this->once()) + ->method('getOptionId') + ->will($this->returnValue($existOption2Id)); + + $bundleOptionExisting = $this->getMock('\Magento\Bundle\Api\Data\OptionInterface'); + $bundleOptionExisting->expects($this->once()) + ->method('getOptionId') + ->will($this->returnValue($existOption1Id)); + + $bundleOptionNew = $this->getMock('\Magento\Bundle\Api\Data\OptionInterface'); + $bundleOptionNew->expects($this->once()) + ->method('getOptionId') + ->will($this->returnValue(null)); + $this->productMock->expects($this->once())->method('getTypeId')->willReturn('bundle'); $this->productMock->expects($this->once()) ->method('getExtensionAttributes') ->willReturn($this->productExtensionMock); $this->productExtensionMock->expects($this->once()) ->method('getBundleProductOptions') - ->willReturn([$this->productBundleOptionsMock]); + ->willReturn([$bundleOptionExisting, $bundleOptionNew]); + $this->productMock->expects($this->exactly(2))->method('getSku') + ->will($this->returnValue($productSku)); $this->productOptionRepositoryMock->expects($this->once()) + ->method('getList') + ->with($productSku) + ->will($this->returnValue([$existingOption1, $existingOption2])); + + $this->productOptionRepositoryMock + ->expects($this->at(1)) + ->method('save') + ->with($this->productMock, $bundleOptionExisting); + + $this->productOptionRepositoryMock + ->expects($this->at(1)) ->method('save') - ->with($this->productMock, $this->productBundleOptionsMock); + ->with($this->productMock, $bundleOptionNew); + + $this->productOptionRepositoryMock + ->expects($this->once()) + ->method('delete') + ->with($existingOption2); + + $newProductMock = $this->getMockBuilder('Magento\Catalog\Api\Data\ProductInterface') + ->disableOriginalConstructor()->getMock(); + $this->productRepositoryMock->expects($this->once()) + ->method('get') + ->with($productSku, false, null, true) + ->willReturn($newProductMock); $this->assertEquals( - $this->productMock, + $newProductMock, $this->plugin->aroundSave($this->productRepositoryMock, $this->closureMock, $this->productMock) ); } diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/LinksListTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/LinksListTest.php index 86c409a3de09e..0909834ce5914 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/Product/LinksListTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/LinksListTest.php @@ -64,6 +64,7 @@ protected function setUp() 'getIsDefault', 'getSelectionQty', 'getSelectionCanChangeQty', + 'getSelectionId', '__wakeup' ], [], @@ -89,6 +90,7 @@ protected function setUp() public function testLinksList() { $optionId = 665; + $selectionId = 1345; $this->productTypeMock->expects($this->once()) ->method('getSelectionsCollection') ->with([$optionId], $this->productMock) @@ -99,6 +101,7 @@ public function testLinksList() ->willReturn('selection_price_type'); $this->selectionMock->expects($this->once())->method('getSelectionPriceValue')->willReturn(12); $this->selectionMock->expects($this->once())->method('getData')->willReturn(['some data']); + $this->selectionMock->expects($this->once())->method('getSelectionId')->willReturn($selectionId); $this->selectionMock->expects($this->once())->method('getIsDefault')->willReturn(true); $this->selectionMock->expects($this->once())->method('getSelectionQty')->willReturn(66); $this->selectionMock->expects($this->once())->method('getSelectionCanChangeQty')->willReturn(22); @@ -108,8 +111,9 @@ public function testLinksList() ->with($linkMock, ['some data'], '\Magento\Bundle\Api\Data\LinkInterface')->willReturnSelf(); $linkMock->expects($this->once())->method('setIsDefault')->with(true)->willReturnSelf(); $linkMock->expects($this->once())->method('setQty')->with(66)->willReturnSelf(); - $linkMock->expects($this->once())->method('setIsDefined')->with(22)->willReturnSelf(); + $linkMock->expects($this->once())->method('setCanChangeQuantity')->with(22)->willReturnSelf(); $linkMock->expects($this->once())->method('setPrice')->with(12)->willReturnSelf(); + $linkMock->expects($this->once())->method('setId')->with($selectionId)->willReturnSelf(); $linkMock->expects($this->once()) ->method('setPriceType')->with('selection_price_type')->willReturnSelf(); $this->linkFactoryMock->expects($this->once())->method('create')->willReturn($linkMock); diff --git a/app/code/Magento/Bundle/etc/data_object.xml b/app/code/Magento/Bundle/etc/data_object.xml index 88e317dafc78c..2b3da013978f9 100644 --- a/app/code/Magento/Bundle/etc/data_object.xml +++ b/app/code/Magento/Bundle/etc/data_object.xml @@ -8,7 +8,5 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Api/etc/data_object.xsd"> <custom_attributes for="Magento\Catalog\Api\Data\ProductInterface"> <attribute code="bundle_product_options" type="Magento\Bundle\Api\Data\OptionInterface[]" /> - <attribute code="price_type" type="integer" /> - <attribute code="price_view" type="string" /> </custom_attributes> </config> diff --git a/app/code/Magento/Bundle/etc/webapi.xml b/app/code/Magento/Bundle/etc/webapi.xml index ec0dffcf1049e..550987ba13e66 100644 --- a/app/code/Magento/Bundle/etc/webapi.xml +++ b/app/code/Magento/Bundle/etc/webapi.xml @@ -13,7 +13,13 @@ <resource ref="Magento_Catalog::products"/> </resources> </route> - <route url="/V1/bundle-products/:productId/children" method="GET"> + <route url="/V1/bundle-products/:sku/links/:id" method="PUT"> + <service class="Magento\Bundle\Api\ProductLinkManagementInterface" method="saveChild"/> + <resources> + <resource ref="Magento_Catalog::products"/> + </resources> + </route> + <route url="/V1/bundle-products/:productSku/children" method="GET"> <service class="Magento\Bundle\Api\ProductLinkManagementInterface" method="getChildren"/> <resources> <resource ref="Magento_Catalog::products"/> diff --git a/app/code/Magento/Catalog/Api/ProductRepositoryInterface.php b/app/code/Magento/Catalog/Api/ProductRepositoryInterface.php index 8205f68fca609..7fb25a7c046f5 100644 --- a/app/code/Magento/Catalog/Api/ProductRepositoryInterface.php +++ b/app/code/Magento/Catalog/Api/ProductRepositoryInterface.php @@ -27,10 +27,11 @@ public function save(\Magento\Catalog\Api\Data\ProductInterface $product, $saveO * @param string $sku * @param bool $editMode * @param null|int $storeId + * @param bool $forceReload * @return \Magento\Catalog\Api\Data\ProductInterface * @throws \Magento\Framework\Exception\NoSuchEntityException */ - public function get($sku, $editMode = false, $storeId = null); + public function get($sku, $editMode = false, $storeId = null, $forceReload = false); /** * Get info about product by product id @@ -38,10 +39,11 @@ public function get($sku, $editMode = false, $storeId = null); * @param int $productId * @param bool $editMode * @param null|int $storeId + * @param bool $forceReload * @return \Magento\Catalog\Api\Data\ProductInterface * @throws \Magento\Framework\Exception\NoSuchEntityException */ - public function getById($productId, $editMode = false, $storeId = null); + public function getById($productId, $editMode = false, $storeId = null, $forceReload = false); /** * Delete product diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php index 9dbbddcee68f8..318fad719e5c0 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php @@ -64,6 +64,8 @@ public function __construct( public function initialize(\Magento\Catalog\Model\Product $product) { $productData = $this->request->getPost('product'); + unset($productData['custom_attributes']); + unset($productData['extension_attributes']); if ($productData) { $stockData = isset($productData['stock_data']) ? $productData['stock_data'] : []; diff --git a/app/code/Magento/Catalog/Model/Plugin/ProductRepository/TransactionWrapper.php b/app/code/Magento/Catalog/Model/Plugin/ProductRepository/TransactionWrapper.php new file mode 100644 index 0000000000000..1d940a5d7aee5 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Plugin/ProductRepository/TransactionWrapper.php @@ -0,0 +1,102 @@ +<?php +/** + * Plugin for \Magento\Catalog\Api\ProductRepositoryInterface + * + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Model\Plugin\ProductRepository; + +class TransactionWrapper +{ + /** + * @var \Magento\Catalog\Model\Resource\Product + */ + protected $resourceModel; + + /** + * @param \Magento\Catalog\Model\Resource\Product $resourceModel + */ + public function __construct( + \Magento\Catalog\Model\Resource\Product $resourceModel + ) { + $this->resourceModel = $resourceModel; + } + + /** + * @param \Magento\Catalog\Api\ProductRepositoryInterface $subject + * @param callable $proceed + * @param \Magento\Catalog\Api\Data\ProductInterface $product + * @param bool $saveOptions + * @return \Magento\Catalog\Api\Data\ProductInterface + * @throws \Exception + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundSave( + \Magento\Catalog\Api\ProductRepositoryInterface $subject, + \Closure $proceed, + \Magento\Catalog\Api\Data\ProductInterface $product, + $saveOptions = false + ) { + $this->resourceModel->beginTransaction(); + try { + /** @var \Magento\Catalog\Api\Data\ProductInterface $result */ + $result = $proceed($product, $saveOptions); + $this->resourceModel->commit(); + return $result; + } catch (\Exception $e) { + $this->resourceModel->rollBack(); + throw $e; + } + } + + /** + * @param \Magento\Catalog\Api\ProductRepositoryInterface $subject + * @param callable $proceed + * @param \Magento\Catalog\Api\Data\ProductInterface $product + * @return bool + * @throws \Exception + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundDelete( + \Magento\Catalog\Api\ProductRepositoryInterface $subject, + \Closure $proceed, + \Magento\Catalog\Api\Data\ProductInterface $product + ) { + $this->resourceModel->beginTransaction(); + try { + /** @var bool $result */ + $result = $proceed($product); + $this->resourceModel->commit(); + return $result; + } catch (\Exception $e) { + $this->resourceModel->rollBack(); + throw $e; + } + } + + /** + * @param \Magento\Catalog\Api\ProductRepositoryInterface $subject + * @param callable $proceed + * @param string $productSku + * @return bool + * @throws \Exception + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundDeleteById( + \Magento\Catalog\Api\ProductRepositoryInterface $subject, + \Closure $proceed, + $productSku + ) { + $this->resourceModel->beginTransaction(); + try { + /** @var bool $result */ + $result = $proceed($productSku); + $this->resourceModel->commit(); + return $result; + } catch (\Exception $e) { + $this->resourceModel->rollBack(); + throw $e; + } + } +} diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php index 43fa06093960b..40ecc5d2cb341 100644 --- a/app/code/Magento/Catalog/Model/ProductRepository.php +++ b/app/code/Magento/Catalog/Model/ProductRepository.php @@ -181,10 +181,10 @@ public function __construct( /** * {@inheritdoc} */ - public function get($sku, $editMode = false, $storeId = null) + public function get($sku, $editMode = false, $storeId = null, $forceReload = false) { $cacheKey = $this->getCacheKey(func_get_args()); - if (!isset($this->instances[$sku][$cacheKey])) { + if (!isset($this->instances[$sku][$cacheKey]) || $forceReload) { $product = $this->productFactory->create(); $productId = $this->resourceModel->getIdBySku($sku); @@ -204,10 +204,10 @@ public function get($sku, $editMode = false, $storeId = null) /** * {@inheritdoc} */ - public function getById($productId, $editMode = false, $storeId = null) + public function getById($productId, $editMode = false, $storeId = null, $forceReload = false) { $cacheKey = $this->getCacheKey(func_get_args()); - if (!isset($this->instancesById[$productId][$cacheKey])) { + if (!isset($this->instancesById[$productId][$cacheKey]) || $forceReload) { $product = $this->productFactory->create(); if ($editMode) { @@ -235,6 +235,7 @@ public function getById($productId, $editMode = false, $storeId = null) protected function getCacheKey($data) { unset($data[0]); + unset($data['forceReload']); $serializeData = []; foreach ($data as $key => $value) { if (is_object($value)) { diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Plugin/ProductRepository/TransactionWrapperTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Plugin/ProductRepository/TransactionWrapperTest.php new file mode 100644 index 0000000000000..681af582d1344 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Plugin/ProductRepository/TransactionWrapperTest.php @@ -0,0 +1,141 @@ +<?php +/** + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Test\Unit\Model\Plugin\ProductRepository; + +class TransactionWrapperTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Catalog\Model\Plugin\ProductRepository\TransactionWrapper + */ + protected $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Catalog\Model\Resource\Product + */ + protected $resourceMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Catalog\Api\ProductRepositoryInterface + */ + protected $subjectMock; + + /** + * @var \Closure + */ + protected $closureMock; + + /** + * @var \Closure + */ + protected $rollbackClosureMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $productMock; + + /** + * @var bool + */ + protected $saveOption = true; + + const ERROR_MSG = "error occurred"; + + protected function setUp() + { + $this->resourceMock = $this->getMock('Magento\Catalog\Model\Resource\Product', [], [], '', false); + $this->subjectMock = $this->getMock('Magento\Catalog\Api\ProductRepositoryInterface', [], [], '', false); + $this->productMock = $this->getMock('Magento\Catalog\Api\Data\ProductInterface', [], [], '', false); + $productMock = $this->productMock; + $this->closureMock = function () use ($productMock) { + return $productMock; + }; + $this->rollbackClosureMock = function () use ($productMock) { + throw new \Exception(self::ERROR_MSG); + }; + + $this->model = new \Magento\Catalog\Model\Plugin\ProductRepository\TransactionWrapper($this->resourceMock); + } + + public function testAroundSaveCommit() + { + $this->resourceMock->expects($this->once())->method('beginTransaction'); + $this->resourceMock->expects($this->once())->method('commit'); + + $this->assertEquals( + $this->productMock, + $this->model->aroundSave($this->subjectMock, $this->closureMock, $this->productMock, $this->saveOption) + ); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage error occurred + */ + public function testAroundSaveRollBack() + { + $this->resourceMock->expects($this->once())->method('beginTransaction'); + $this->resourceMock->expects($this->once())->method('rollBack'); + + $this->model->aroundSave($this->subjectMock, $this->rollbackClosureMock, $this->productMock, $this->saveOption); + } + + public function testAroundDeleteCommit() + { + $this->resourceMock->expects($this->once())->method('beginTransaction'); + $this->resourceMock->expects($this->once())->method('commit'); + + $this->assertEquals( + $this->productMock, + $this->model->aroundDelete($this->subjectMock, $this->closureMock, $this->productMock, $this->saveOption) + ); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage error occurred + */ + public function testAroundDeleteRollBack() + { + $this->resourceMock->expects($this->once())->method('beginTransaction'); + $this->resourceMock->expects($this->once())->method('rollBack'); + + $this->model->aroundDelete( + $this->subjectMock, + $this->rollbackClosureMock, + $this->productMock, + $this->saveOption + ); + } + + public function testAroundDeleteByIdCommit() + { + $this->resourceMock->expects($this->once())->method('beginTransaction'); + $this->resourceMock->expects($this->once())->method('commit'); + + $this->assertEquals( + $this->productMock, + $this->model->aroundDelete($this->subjectMock, $this->closureMock, $this->productMock, $this->saveOption) + ); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage error occurred + */ + public function testAroundDeleteByIdRollBack() + { + $this->resourceMock->expects($this->once())->method('beginTransaction'); + $this->resourceMock->expects($this->once())->method('rollBack'); + + $this->model->aroundDelete( + $this->subjectMock, + $this->rollbackClosureMock, + $this->productMock, + $this->saveOption + ); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductLink/RepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductLink/RepositoryTest.php index affded52e9586..26f70422f0e60 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductLink/RepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductLink/RepositoryTest.php @@ -75,8 +75,8 @@ public function testSave() $linkedProductMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); $this->productRepositoryMock->expects($this->exactly(2))->method('get')->will($this->returnValueMap( [ - ['product', false, null, $productMock], - ['linkedProduct', false, null, $linkedProductMock], + ['product', false, null, false, $productMock], + ['linkedProduct', false, null, false, $linkedProductMock], ] )); $entityMock->expects($this->once())->method('getLinkedProductSku')->willReturn('linkedProduct'); @@ -102,8 +102,8 @@ public function testSaveWithException() $linkedProductMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); $this->productRepositoryMock->expects($this->exactly(2))->method('get')->will($this->returnValueMap( [ - ['product', false, null, $productMock], - ['linkedProduct', false, null, $linkedProductMock], + ['product', false, null, false, $productMock], + ['linkedProduct', false, null, false, $linkedProductMock], ] )); $entityMock->expects($this->once())->method('getLinkedProductSku')->willReturn('linkedProduct'); @@ -129,8 +129,8 @@ public function testDelete() $linkedProductMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); $this->productRepositoryMock->expects($this->exactly(2))->method('get')->will($this->returnValueMap( [ - ['product', false, null, $productMock], - ['linkedProduct', false, null, $linkedProductMock], + ['product', false, null, false, $productMock], + ['linkedProduct', false, null, false, $linkedProductMock], ] )); $entityMock->expects($this->once())->method('getLinkedProductSku')->willReturn('linkedProduct'); @@ -157,8 +157,8 @@ public function testDeleteWithInvalidDataException() $linkedProductMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); $this->productRepositoryMock->expects($this->exactly(2))->method('get')->will($this->returnValueMap( [ - ['product', false, null, $productMock], - ['linkedProduct', false, null, $linkedProductMock], + ['product', false, null, false, $productMock], + ['linkedProduct', false, null, false, $linkedProductMock], ] )); $entityMock->expects($this->once())->method('getLinkedProductSku')->willReturn('linkedProduct'); @@ -186,8 +186,8 @@ public function testDeleteWithNoSuchEntityException() $linkedProductMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); $this->productRepositoryMock->expects($this->exactly(2))->method('get')->will($this->returnValueMap( [ - ['product', false, null, $productMock], - ['linkedProduct', false, null, $linkedProductMock], + ['product', false, null, false, $productMock], + ['linkedProduct', false, null, false, $linkedProductMock], ] )); $entityMock->expects($this->exactly(2))->method('getLinkedProductSku')->willReturn('linkedProduct'); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php index d50868fc2306b..a8ba184519d79 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php @@ -298,6 +298,55 @@ public function testGetByIdForCacheKeyGenerate($identifier, $editMode, $storeId) $this->productMock->expects($this->once())->method('load')->with($identifier); $this->productMock->expects($this->once())->method('getId')->willReturn($identifier); $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); + //Second invocation should just return from cache + $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); + } + + /** + * Test the forceReload parameter + * + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testGetByIdForcedReload() + { + $identifier = "23"; + $editMode = false; + $storeId = 0; + + $this->productFactoryMock->expects($this->exactly(2))->method('create') + ->will($this->returnValue($this->productMock)); + $this->productMock->expects($this->exactly(2))->method('load'); + $this->productMock->expects($this->exactly(2))->method('getId')->willReturn($identifier); + $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); + //second invocation should just return from cache + $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); + //force reload + $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId, true)); + } + + /** + * Test forceReload parameter + * + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testGetForcedReload() + { + $sku = "sku"; + $id = "23"; + $editMode = false; + $storeId = 0; + + $this->productFactoryMock->expects($this->exactly(2))->method('create') + ->will($this->returnValue($this->productMock)); + $this->productMock->expects($this->exactly(2))->method('load'); + $this->productMock->expects($this->exactly(2))->method('getId')->willReturn($sku); + $this->resourceModelMock->expects($this->exactly(2))->method('getIdBySku') + ->with($sku)->willReturn($id); + $this->assertEquals($this->productMock, $this->model->get($sku, $editMode, $storeId)); + //second invocation should just return from cache + $this->assertEquals($this->productMock, $this->model->get($sku, $editMode, $storeId)); + //force reload + $this->assertEquals($this->productMock, $this->model->get($sku, $editMode, $storeId, true)); } public function testGetByIdWithSetStoreId() diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index 971bebcac3226..acd741fdbbe66 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -461,4 +461,7 @@ <preference for="Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface" type="\Magento\Catalog\Model\Product\Option\Value" /> <virtualType name="Magento\Catalog\Model\Resource\Attribute\Collection" type="Magento\Eav\Model\Resource\Entity\Attribute\Collection"> </virtualType> + <type name="Magento\Catalog\Api\ProductRepositoryInterface"> + <plugin name="transactionWrapper" type="\Magento\Catalog\Model\Plugin\ProductRepository\TransactionWrapper" sortOrder="-1"/> + </type> </config> diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Item.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Item.php index 163a03903f878..35d522427429f 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Item.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Item.php @@ -219,7 +219,7 @@ public function calcRowTotal() $rowTotalInclTax = $orderItem->getRowTotalInclTax(); $baseRowTotalInclTax = $orderItem->getBaseRowTotalInclTax(); - if (!$this->isLast() && $orderItemQtyInvoiced > 0 && $this->getQty() > 0) { + if (!$this->isLast() && $orderItemQtyInvoiced > 0 && $this->getQty() >= 0) { $availableQty = $orderItemQtyInvoiced - $orderItem->getQtyRefunded(); $rowTotal = $creditmemo->roundPrice($rowTotal / $availableQty * $this->getQty()); $baseRowTotal = $creditmemo->roundPrice($baseRowTotal / $availableQty * $this->getQty(), 'base'); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/ItemTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/ItemTest.php index afcf792c718dd..0673aa120a217 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/ItemTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/ItemTest.php @@ -264,43 +264,53 @@ public function testCancel() $this->assertInstanceOf('Magento\Sales\Model\Order\Creditmemo\Item', $result); } - public function testCalcRowTotal() + /** + * @dataProvider calcRowTotalDataProvider + */ + public function testCalcRowTotal($qty) { $creditmemoMock = $this->getMockBuilder('\Magento\Sales\Model\Order\Creditmemo') ->disableOriginalConstructor() ->getMock(); $creditmemoMock->expects($this->exactly(4)) ->method('roundPrice') - ->willReturnMap( - [ - [0.375, 'regular', false, 0.4], - [0.375, 'base', false, 0.4], - [1, 'including', false, 1.0], - [1, 'including_base', false, 1.0] - ] - ); + ->will($this->returnCallback( + function ($arg) { + return round($arg, 2); + } + )); + + $qtyInvoiced = 10; + $qtyRefunded = 2; + $qtyAvailable = $qtyInvoiced - $qtyRefunded; + + $rowInvoiced = 5; + $amountRefunded = 2; + + $expectedRowTotal = ($rowInvoiced - $amountRefunded) / $qtyAvailable * $qty; + $expectedRowTotal = round($expectedRowTotal, 2); $orderItemMock = $this->getMockBuilder('Magento\Sales\Model\Order\Item') ->disableOriginalConstructor() ->getMock(); $orderItemMock->expects($this->once()) ->method('getQtyInvoiced') - ->willReturn(10); + ->willReturn($qtyInvoiced); $orderItemMock->expects($this->once()) ->method('getQtyRefunded') - ->willReturn(2); + ->willReturn($qtyRefunded); $orderItemMock->expects($this->once()) ->method('getRowInvoiced') - ->willReturn(5); + ->willReturn($rowInvoiced); $orderItemMock->expects($this->once()) ->method('getAmountRefunded') - ->willReturn(2); + ->willReturn($amountRefunded); $orderItemMock->expects($this->once()) ->method('getBaseRowInvoiced') - ->willReturn(5); + ->willReturn($rowInvoiced); $orderItemMock->expects($this->once()) ->method('getBaseAmountRefunded') - ->willReturn(2); + ->willReturn($amountRefunded); $orderItemMock->expects($this->once()) ->method('getRowTotalInclTax') ->willReturn(1); @@ -313,11 +323,28 @@ public function testCalcRowTotal() $orderItemMock->expects($this->once()) ->method('getQtyOrdered') ->willReturn(1); + $orderItemMock->expects($this->any()) + ->method('getQtyToRefund') + ->willReturn($qtyAvailable); - $this->item->setData('qty', 1); + $this->item->setData('qty', $qty); $this->item->setCreditmemo($creditmemoMock); $this->item->setOrderItem($orderItemMock); $result = $this->item->calcRowTotal(); + $this->assertInstanceOf('Magento\Sales\Model\Order\Creditmemo\Item', $result); + $this->assertEquals($expectedRowTotal, $this->item->getData('row_total')); + $this->assertEquals($expectedRowTotal, $this->item->getData('base_row_total')); + } + + /** + * @return array + */ + public function calcRowTotalDataProvider() + { + return [ + 'qty 1' => [1], + 'qty 0' => [0], + ]; } } diff --git a/app/code/Magento/Weee/Model/Total/Creditmemo/Weee.php b/app/code/Magento/Weee/Model/Total/Creditmemo/Weee.php index d45eab656ca2a..7ddf6d66a7d0a 100644 --- a/app/code/Magento/Weee/Model/Total/Creditmemo/Weee.php +++ b/app/code/Magento/Weee/Model/Total/Creditmemo/Weee.php @@ -49,36 +49,30 @@ public function collect(Creditmemo $creditmemo) $totalWeeeAmount = 0; $baseTotalWeeeAmount = 0; - $totalWeeeAmountInclTax = 0; $baseTotalWeeeAmountInclTax = 0; - - $totalTaxAmount = $totalWeeeAmountInclTax - $totalWeeeAmount; - $baseTotalTaxAmount = $baseTotalWeeeAmountInclTax - $baseTotalWeeeAmount; + $totalTaxAmount = 0; + $baseTotalTaxAmount = 0; foreach ($creditmemo->getAllItems() as $item) { $orderItem = $item->getOrderItem(); - if ($orderItem->isDummy() || $item->getQty() <= 0) { + $orderItemQty = $orderItem->getQtyOrdered(); + + if (!$orderItemQty || $orderItem->isDummy() || $item->getQty() < 0) { continue; } - $ratio = $item->getQty() / $orderItem->getQtyOrdered(); + $ratio = $item->getQty() / $orderItemQty; $orderItemWeeeAmountExclTax = $orderItem->getWeeeTaxAppliedRowAmount(); $orderItemBaseWeeeAmountExclTax = $orderItem->getBaseWeeeTaxAppliedRowAmnt(); $weeeAmountExclTax = $creditmemo->roundPrice($orderItemWeeeAmountExclTax * $ratio); - $baseWeeeAmountExclTax = $creditmemo->roundPrice( - $orderItemBaseWeeeAmountExclTax * $ratio, - 'base' - ); + $baseWeeeAmountExclTax = $creditmemo->roundPrice($orderItemBaseWeeeAmountExclTax * $ratio, 'base'); $orderItemWeeeAmountInclTax = $this->_weeeData->getRowWeeeTaxInclTax($orderItem); $orderItemBaseWeeeAmountInclTax = $this->_weeeData->getBaseRowWeeeTaxInclTax($orderItem); $weeeAmountInclTax = $creditmemo->roundPrice($orderItemWeeeAmountInclTax * $ratio); - $baseWeeeAmountInclTax = $creditmemo->roundPrice( - $orderItemBaseWeeeAmountInclTax * $ratio, - 'base' - ); + $baseWeeeAmountInclTax = $creditmemo->roundPrice($orderItemBaseWeeeAmountInclTax * $ratio, 'base'); $itemTaxAmount = $weeeAmountInclTax - $weeeAmountExclTax; $itemBaseTaxAmount = $baseWeeeAmountInclTax - $baseWeeeAmountExclTax; diff --git a/app/code/Magento/Weee/Model/Total/Invoice/Weee.php b/app/code/Magento/Weee/Model/Total/Invoice/Weee.php index 457a08ccd8669..a5bed91ec0c36 100644 --- a/app/code/Magento/Weee/Model/Total/Invoice/Weee.php +++ b/app/code/Magento/Weee/Model/Total/Invoice/Weee.php @@ -57,11 +57,12 @@ public function collect(\Magento\Sales\Model\Order\Invoice $invoice) $orderItem = $item->getOrderItem(); $orderItemQty = $orderItem->getQtyOrdered(); - if (!$orderItemQty || $orderItem->isDummy() || $item->getQty() <= 0) { + if (!$orderItemQty || $orderItem->isDummy() || $item->getQty() < 0) { continue; } $ratio = $item->getQty() / $orderItemQty; + $orderItemWeeeAmount = $orderItem->getWeeeTaxAppliedRowAmount(); $orderItemBaseWeeeAmount = $orderItem->getBaseWeeeTaxAppliedRowAmnt(); $weeeAmount = $invoice->roundPrice($orderItemWeeeAmount * $ratio); diff --git a/app/code/Magento/Weee/Test/Unit/Model/Total/Creditmemo/WeeeTest.php b/app/code/Magento/Weee/Test/Unit/Model/Total/Creditmemo/WeeeTest.php index 47aa7692a51d9..0fdd0ad9bdab1 100644 --- a/app/code/Magento/Weee/Test/Unit/Model/Total/Creditmemo/WeeeTest.php +++ b/app/code/Magento/Weee/Test/Unit/Model/Total/Creditmemo/WeeeTest.php @@ -169,6 +169,7 @@ function ($price, $type) use (&$roundingDelta) { public function collectDataProvider() { $result = []; + // scenario 1: 3 item_1, $100 with $weee, 8.25 tax rate, 3 items invoiced, full creditmemo $result['complete_creditmemo'] = [ 'creditmemo_data' => [ @@ -236,7 +237,6 @@ public function collectDataProvider() 'tax_ratio' => serialize(['weee' => 1.0]), 'weee_tax_applied_row_amount' => 30, 'base_weee_tax_applied_row_amount' => 30, - ], ], 'creditmemo_data' => [ @@ -248,7 +248,6 @@ public function collectDataProvider() 'base_subtotal' => 300, 'subtotal_incl_tax' => 357.22, 'base_subtotal_incl_tax' => 357.22, - ], ], ]; @@ -320,7 +319,6 @@ public function collectDataProvider() 'tax_ratio' => serialize(['weee' => 1.65 / 2.47]), 'weee_tax_applied_row_amount' => 20, 'base_weee_tax_applied_row_amount' => 20, - ], ], 'creditmemo_data' => [ @@ -332,7 +330,6 @@ public function collectDataProvider() 'base_subtotal' => 200, 'subtotal_incl_tax' => 238.15, 'base_subtotal_incl_tax' => 238.15, - ], ], ]; @@ -404,7 +401,6 @@ public function collectDataProvider() 'tax_ratio' => serialize(['weee' => 0.83 / 2.47]), 'weee_tax_applied_row_amount' => 10, 'base_weee_tax_applied_row_amount' => 10, - ], ], 'creditmemo_data' => [ @@ -416,7 +412,79 @@ public function collectDataProvider() 'base_subtotal' => 100, 'subtotal_incl_tax' => 119.07, 'base_subtotal_incl_tax' => 119.07, + ], + ], + ]; + // scenario 4: 3 item_1, $100 with $weee, 8.25 tax rate. Returning qty 0. + $result['zero_return'] = [ + 'creditmemo_data' => [ + 'items' => [ + 'item_1' => [ + 'order_item' => [ + 'qty_ordered' => 3, + 'weee_tax_applied_row_amount' => 30, + 'base_weee_tax_applied_row_amnt' => 30, + 'row_weee_tax_incl_tax' => 32.47, + 'base_row_weee_tax_incl_tax' => 32.47, + 'weee_amount_invoiced' => 30, + 'base_weee_amount_invoiced' => 30, + 'weee_amount_refunded' => 0, + 'base_weee_amount_refunded' => 0, + 'weee_tax_amount_invoiced' => 2.47, + 'base_weee_tax_amount_invoiced' => 2.47, + 'weee_tax_amount_refunded' => 0, + 'base_weee_tax_amount_refunded' => 0, + 'applied_weee' => [ + [ + 'title' => 'recycling_fee', + 'base_row_amount' => 30, + 'row_amount' => 30, + 'base_row_amount_incl_tax' => 32.47, + 'row_amount_incl_tax' => 32.47, + ], + ], + 'qty_invoiced' => 3, + ], + 'is_last' => true, + 'data_fields' => [ + 'qty' => 0, + 'applied_weee' => [ + [ + ], + ], + ], + ], + ], + 'include_in_subtotal' => false, + 'data_fields' => [ + 'grand_total' => 300, + 'base_grand_total' => 300, + 'subtotal' => 300, + 'base_subtotal' => 300, + 'subtotal_incl_tax' => 324.75, + 'base_subtotal_incl_tax' => 324.75, + 'tax_amount' => 0, + 'base_tax_amount' => 0, + ], + ], + 'expected_results' => [ + 'creditmemo_items' => [ + 'item_1' => [ + 'applied_weee' => [ + [ + 'title' => 'recycling_fee', + 'base_row_amount' => 0, + 'row_amount' => 0, + 'base_row_amount_incl_tax' => 0, + 'row_amount_incl_tax' => 0, + ], + ], + ], + ], + 'creditmemo_data' => [ + 'subtotal' => 300, + 'base_subtotal' => 300, ], ], ]; diff --git a/app/code/Magento/Weee/Test/Unit/Model/Total/Invoice/WeeeTest.php b/app/code/Magento/Weee/Test/Unit/Model/Total/Invoice/WeeeTest.php index 4d91954b70fa2..d034947c48d71 100644 --- a/app/code/Magento/Weee/Test/Unit/Model/Total/Invoice/WeeeTest.php +++ b/app/code/Magento/Weee/Test/Unit/Model/Total/Invoice/WeeeTest.php @@ -172,6 +172,7 @@ function ($price, $type) use (&$roundingDelta) { public function collectDataProvider() { $result = []; + // 3 item_1, $100 with $weee, 8.25 tax rate, full invoice $result['complete_invoice'] = [ 'order_data' => [ @@ -269,7 +270,6 @@ public function collectDataProvider() 'base_subtotal' => 300, 'subtotal_incl_tax' => 344.85, 'base_subtotal_incl_tax' => 344.85, - ], ], ]; @@ -360,7 +360,6 @@ public function collectDataProvider() 'tax_ratio' => serialize(['weee' => 1.65 / 2.47]), 'weee_tax_applied_row_amount' => 20, 'base_weee_tax_applied_row_amount' => 20, - ], ], 'invoice_data' => [ @@ -372,7 +371,6 @@ public function collectDataProvider() 'base_subtotal' => 200, 'subtotal_incl_tax' => 238.15, 'base_subtotal_incl_tax' => 238.15, - ], ], ]; @@ -464,7 +462,6 @@ public function collectDataProvider() 'tax_ratio' => serialize(['weee' => 0.82 / 2.47]), 'weee_tax_applied_row_amount' => 10, 'base_weee_tax_applied_row_amount' => 10, - ], ], 'invoice_data' => [ @@ -476,7 +473,6 @@ public function collectDataProvider() 'base_subtotal' => 100, 'subtotal_incl_tax' => 119.07, 'base_subtotal_incl_tax' => 119.07, - ], ], ]; @@ -580,7 +576,98 @@ public function collectDataProvider() 'base_subtotal' => 100, 'subtotal_incl_tax' => 114.95, 'base_subtotal_incl_tax' => 114.95, + ], + ], + ]; + // 3 item_1, $100 with $weee, 8.25 tax rate. Invoicing qty 0. + $result['zero_invoice'] = [ + 'order_data' => [ + 'previous_invoices' => [ + ], + 'data_fields' => [ + 'shipping_tax_amount' => 1.24, + 'base_shipping_tax_amount' => 1.24, + 'shipping_hidden_tax_amount' => 0, + 'base_shipping_hidden_tax_amount' => 0, + 'tax_amount' => 16.09, + 'tax_invoiced' => 0, + 'base_tax_amount' => 16.09, + 'base_tax_amount_invoiced' => 0, + 'subtotal' => '300', + 'base_subtotal' => '300', + ], + ], + 'invoice_data' => [ + 'items' => [ + 'item_1' => [ + 'order_item' => [ + 'qty_ordered' => 3, + 'weee_tax_applied_row_amount' => 30, + 'base_weee_tax_applied_row_amnt' => 30, + 'row_weee_tax_incl_tax' => 32.47, + 'base_row_weee_tax_incl_tax' => 32.47, + 'weee_amount_invoiced' => 0, + 'base_weee_amount_invoiced' => 0, + 'weee_tax_amount_invoiced' => 0, + 'base_weee_tax_amount_invoiced' => 0, + 'applied_weee' => [ + [ + 'title' => 'recycling_fee', + 'base_row_amount' => 30, + 'row_amount' => 30, + 'base_row_amount_incl_tax' => 32.47, + 'row_amount_incl_tax' => 32.47, + ], + ], + 'applied_weee_updated' => [ + 'base_row_amount_invoiced' => 30, + 'row_amount_invoiced' => 30, + 'base_tax_amount_invoiced' => 2.47, + 'tax_amount_invoiced' => 2.47, + ], + 'qty_invoiced' => 0, + ], + 'is_last' => true, + 'data_fields' => [ + 'qty' => 0, + 'applied_weee' => [ + [ + ], + ], + ], + ], + ], + 'is_last' => true, + 'include_in_subtotal' => false, + 'data_fields' => [ + 'grand_total' => 181.09, + 'base_grand_total' => 181.09, + 'subtotal' => 300, + 'base_subtotal' => 300, + 'subtotal_incl_tax' => 314.85, + 'base_subtotal_incl_tax' => 314.85, + 'tax_amount' => 16.09, + 'base_tax_amount' => 16.09, + ], + ], + 'expected_results' => [ + 'invoice_items' => [ + 'item_1' => [ + 'applied_weee' => [ + [ + 'title' => 'recycling_fee', + 'base_row_amount' => 0, + 'row_amount' => 0, + 'base_row_amount_incl_tax' => 0, + 'row_amount_incl_tax' => 0, + ], + ], + ], + ], + 'invoice_data' => [ + 'subtotal' => 300, + 'base_subtotal' => 300, ], ], ]; diff --git a/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductLinkManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductLinkManagementTest.php index 463f490480ce0..caf1b320ef82c 100644 --- a/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductLinkManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductLinkManagementTest.php @@ -33,12 +33,13 @@ public function testGetChildren() $this->assertArrayHasKey(0, $result); $this->assertArrayHasKey('option_id', $result[0]); $this->assertArrayHasKey('is_default', $result[0]); - $this->assertArrayHasKey('is_defined', $result[0]); + $this->assertArrayHasKey('can_change_quantity', $result[0]); $this->assertArrayHasKey('price', $result[0]); $this->assertArrayHasKey('price_type', $result[0]); + $this->assertNotNull($result[0]['id']); - unset($result[0]['option_id'], $result[0]['is_default'], $result[0]['is_defined']); - unset($result[0]['price'], $result[0]['price_type']); + unset($result[0]['option_id'], $result[0]['is_default'], $result[0]['can_change_quantity']); + unset($result[0]['price'], $result[0]['price_type'], $result[0]['id']); ksort($result[0]); ksort($expected[0]); @@ -83,6 +84,55 @@ public function testAddChild() $this->assertGreaterThan(0, $childId); } + /** + * @magentoApiDataFixture Magento/Bundle/_files/product.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + */ + public function testSaveChild() + { + $productSku = 'bundle-product'; + $children = $this->getChildren($productSku); + + $linkedProduct = $children[0]; + + //Modify a few fields + $linkedProduct['is_default'] = true; + $linkedProduct['qty'] = 2; + + $this->assertTrue($this->saveChild($productSku, $linkedProduct)); + $children = $this->getChildren($productSku); + $this->assertEquals($linkedProduct, $children[0]); + } + + /** + * @param string $productSku + * @param array $linkedProduct + * @return string + */ + private function saveChild($productSku, $linkedProduct) + { + $resourcePath = self::RESOURCE_PATH . '/:sku/links/:id'; + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => str_replace( + [':sku', ':id'], + [$productSku, $linkedProduct['id']], + $resourcePath + ), + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'SaveChild', + ], + ]; + return $this->_webApiCall( + $serviceInfo, + ['sku' => $productSku, 'linkedProduct' => $linkedProduct] + ); + } + /** * @param string $productSku * @param int $optionId @@ -158,6 +208,6 @@ protected function getChildren($productSku) 'operation' => self::SERVICE_NAME . 'getChildren', ], ]; - return $this->_webApiCall($serviceInfo, ['productId' => $productSku]); + return $this->_webApiCall($serviceInfo, ['productSku' => $productSku]); } } diff --git a/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductOptionRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductOptionRepositoryTest.php index 325e35ee82910..d402aea3d67a7 100644 --- a/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductOptionRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductOptionRepositoryTest.php @@ -29,7 +29,7 @@ public function testGet() 'sku' => 'simple', 'qty' => 1, 'position' => 0, - 'is_defined' => true, + 'can_change_quantity' => 1, 'is_default' => false, 'price' => null, 'price_type' => null, @@ -42,9 +42,13 @@ public function testGet() $this->assertArrayHasKey('option_id', $result); $expected['product_links'][0]['option_id'] = $result['option_id']; unset($result['option_id']); + $this->assertNotNull($result['product_links'][0]['id']); + unset($result['product_links'][0]['id']); ksort($expected); ksort($result); + ksort($expected['product_links'][0]); + ksort($result['product_links'][0]); $this->assertEquals($expected, $result); } @@ -66,7 +70,7 @@ public function testGetList() 'sku' => 'simple', 'qty' => 1, 'position' => 0, - 'is_defined' => true, + 'can_change_quantity' => 1, 'is_default' => false, 'price' => null, 'price_type' => null, @@ -80,9 +84,13 @@ public function testGetList() $this->assertArrayHasKey('option_id', $result[0]); $expected[0]['product_links'][0]['option_id'] = $result[0]['option_id']; unset($result[0]['option_id']); + $this->assertNotNull($result[0]['product_links'][0]['id']); + unset($result[0]['product_links'][0]['id']); ksort($expected[0]); ksort($result[0]); + ksort($expected[0]['product_links'][0]); + ksort($result[0]['product_links'][0]); $this->assertEquals($expected, $result); } diff --git a/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php b/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php index 1a817b18d3b59..80fbe1999d334 100644 --- a/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php @@ -10,6 +10,7 @@ use Magento\Framework\Api\ExtensibleDataInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; +use Magento\Bundle\Api\Data\LinkInterface; /** * Class ProductServiceTest for testing Bundle Product API @@ -19,6 +20,7 @@ class ProductServiceTest extends WebapiAbstract const SERVICE_NAME = 'catalogProductRepositoryV1'; const SERVICE_VERSION = 'V1'; const RESOURCE_PATH = '/V1/products'; + const BUNDLE_PRODUCT_ID = 'sku-test-product-bundle'; /** * @var \Magento\Catalog\Model\Resource\Product\Collection @@ -39,20 +41,7 @@ public function setUp() */ public function tearDown() { - /** @var \Magento\Framework\Registry $registry */ - $registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get('Magento\Framework\Registry'); - - $registry->unregister('isSecureArea'); - $registry->register('isSecureArea', true); - - $this->productCollection->addFieldToFilter( - 'sku', - ['in' => ['sku-test-product-bundle']] - )->delete(); - unset($this->productCollection); - - $registry->unregister('isSecureArea'); - $registry->register('isSecureArea', false); + $this->deleteProductBySku(self::BUNDLE_PRODUCT_ID); parent::tearDown(); } @@ -61,7 +50,6 @@ public function tearDown() */ public function testCreateBundle() { - $this->markTestSkipped('Processing of custom attributes has been changed in MAGETWO-34448.'); $bundleProductOptions = [ [ "title" => "test option", @@ -73,38 +61,365 @@ public function testCreateBundle() "qty" => 1, 'is_default' => false, 'price' => 1.0, - 'price_type' => 1 + 'price_type' => LinkInterface::PRICE_TYPE_FIXED, ], ], ], ]; - $uniqueId = 'sku-test-product-bundle'; $product = [ - "sku" => $uniqueId, - "name" => $uniqueId, + "sku" => self::BUNDLE_PRODUCT_ID, + "name" => self::BUNDLE_PRODUCT_ID, "type_id" => "bundle", "price" => 50, 'attribute_set_id' => 4, + "custom_attributes" => [ + [ + "attribute_code" => "price_type", + "value" => \Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED, + ], + [ + "attribute_code" => "price_view", + "value" => 1, + ], + ], "extension_attributes" => [ - "price_type" => \Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC, "bundle_product_options" => $bundleProductOptions, - "price_view" => "test" ], ]; $response = $this->createProduct($product); - $this->assertEquals($uniqueId, $response[ProductInterface::SKU]); + $this->assertEquals(self::BUNDLE_PRODUCT_ID, $response[ProductInterface::SKU]); + $this->assertEquals(50, $response['price']); + $this->assertTrue( + isset($response[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]["bundle_product_options"]) + ); + $resultBundleProductOptions + = $response[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]["bundle_product_options"]; + $this->assertTrue(isset($resultBundleProductOptions[0]["product_links"][0]["sku"])); + $this->assertEquals('simple', $resultBundleProductOptions[0]["product_links"][0]["sku"]); + + $response = $this->getProduct(self::BUNDLE_PRODUCT_ID); + $this->assertTrue( + isset($response[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]["bundle_product_options"]) + ); + $resultBundleProductOptions + = $response[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]["bundle_product_options"]; + $this->assertTrue(isset($resultBundleProductOptions[0]["product_links"][0]["sku"])); + $this->assertEquals('simple', $resultBundleProductOptions[0]["product_links"][0]["sku"]); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products_new.php + * @magentoApiDataFixture Magento/Catalog/_files/second_product_simple.php + */ + public function testUpdateBundleModifyExistingSelection() + { + $bundleProduct = $this->createFixedPriceBundleProduct(); + $bundleProductOptions = $this->getBundleProductOptions($bundleProduct); + + $existingSelectionId = $bundleProductOptions[0]['product_links'][0]['id']; + + //Change the type of existing option + $bundleProductOptions[0]['type'] = 'select'; + //Change the sku of existing link and qty + $bundleProductOptions[0]['product_links'][0]['sku'] = 'simple2'; + $bundleProductOptions[0]['product_links'][0]['qty'] = 2; + $bundleProductOptions[0]['product_links'][0]['price'] = 10; + $bundleProductOptions[0]['product_links'][0]['price_type'] = 1; + $this->setBundleProductOptions($bundleProduct, $bundleProductOptions); + + $updatedProduct = $this->saveProduct($bundleProduct); + + $bundleOptions = $this->getBundleProductOptions($updatedProduct); + $this->assertEquals('select', $bundleOptions[0]['type']); + $this->assertEquals('simple2', $bundleOptions[0]['product_links'][0]['sku']); + $this->assertEquals(2, $bundleOptions[0]['product_links'][0]['qty']); + $this->assertEquals($existingSelectionId, $bundleOptions[0]['product_links'][0]['id']); + $this->assertEquals(10, $bundleOptions[0]['product_links'][0]['price']); + $this->assertEquals(1, $bundleOptions[0]['product_links'][0]['price_type']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products_new.php + * @magentoApiDataFixture Magento/Catalog/_files/second_product_simple.php + */ + public function testUpdateBundleModifyExistingOptionOnly() + { + $bundleProduct = $this->createFixedPriceBundleProduct(); + $bundleProductOptions = $this->getBundleProductOptions($bundleProduct); + + $existingSelectionId = $bundleProductOptions[0]['product_links'][0]['id']; + + //Change the type of existing option + $bundleProductOptions[0]['type'] = 'select'; + //unset product_links attribute + unset($bundleProductOptions[0]['product_links']); + $this->setBundleProductOptions($bundleProduct, $bundleProductOptions); + + $updatedProduct = $this->saveProduct($bundleProduct); + + $bundleOptions = $this->getBundleProductOptions($updatedProduct); + $this->assertEquals('select', $bundleOptions[0]['type']); + $this->assertEquals('simple', $bundleOptions[0]['product_links'][0]['sku']); + $this->assertEquals(1, $bundleOptions[0]['product_links'][0]['qty']); + $this->assertEquals($existingSelectionId, $bundleOptions[0]['product_links'][0]['id']); + $this->assertEquals(20, $bundleOptions[0]['product_links'][0]['price']); + $this->assertEquals(1, $bundleOptions[0]['product_links'][0]['price_type']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products_new.php + * @magentoApiDataFixture Magento/Catalog/_files/second_product_simple.php + */ + public function testUpdateProductWithoutBundleOptions() + { + $bundleProduct = $this->createFixedPriceBundleProduct(); + $bundleProductOptions = $this->getBundleProductOptions($bundleProduct); + + $existingSelectionId = $bundleProductOptions[0]['product_links'][0]['id']; + + //unset bundle_product_options + unset($bundleProductOptions[0]['product_links']); + $this->setBundleProductOptions($bundleProduct, null); + + $updatedProduct = $this->saveProduct($bundleProduct); + + $bundleOptions = $this->getBundleProductOptions($updatedProduct); + $this->assertEquals('checkbox', $bundleOptions[0]['type']); + $this->assertEquals('simple', $bundleOptions[0]['product_links'][0]['sku']); + $this->assertEquals(1, $bundleOptions[0]['product_links'][0]['qty']); + $this->assertEquals($existingSelectionId, $bundleOptions[0]['product_links'][0]['id']); + $this->assertEquals(20, $bundleOptions[0]['product_links'][0]['price']); + $this->assertEquals(1, $bundleOptions[0]['product_links'][0]['price_type']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products_new.php + * @magentoApiDataFixture Magento/Catalog/_files/second_product_simple.php + */ + public function testUpdateBundleAddSelection() + { + $bundleProduct = $this->createDynamicBundleProduct(); + $bundleProductOptions = $this->getBundleProductOptions($bundleProduct); + + //Add a selection to existing option + $bundleProductOptions[0]['product_links'][] = [ + 'sku' => 'simple2', + 'qty' => 2, + "price" => 20, + "price_type" => 1, + "is_default" => false, + ]; + $this->setBundleProductOptions($bundleProduct, $bundleProductOptions); + $updatedProduct = $this->saveProduct($bundleProduct); + + $bundleOptions = $this->getBundleProductOptions($updatedProduct); + $this->assertEquals('simple', $bundleOptions[0]['product_links'][0]['sku']); + $this->assertEquals('simple2', $bundleOptions[0]['product_links'][1]['sku']); + $this->assertEquals(2, $bundleOptions[0]['product_links'][1]['qty']); + $this->assertGreaterThan( + $bundleOptions[0]['product_links'][0]['id'], + $bundleOptions[0]['product_links'][1]['id'] + ); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products_new.php + * @magentoApiDataFixture Magento/Catalog/_files/second_product_simple.php + */ + public function testUpdateBundleAddAndDeleteOption() + { + $bundleProduct = $this->createDynamicBundleProduct(); + + $bundleProductOptions = $this->getBundleProductOptions($bundleProduct); + + $oldOptionId = $bundleProductOptions[0]['option_id']; + //replace current option with a new option + $bundleProductOptions[0] = [ + 'title' => 'new option', + 'required' => true, + 'type' => 'select', + 'product_links' => [ + [ + 'sku' => 'simple2', + 'qty' => 2, + "price" => 20, + "price_type" => 1, + "is_default" => false, + ], + ], + ]; + $this->setBundleProductOptions($bundleProduct, $bundleProductOptions); + $this->saveProduct($bundleProduct); + + $updatedProduct = $this->getProduct(self::BUNDLE_PRODUCT_ID); + $bundleOptions = $this->getBundleProductOptions($updatedProduct); + $this->assertEquals('new option', $bundleOptions[0]['title']); + $this->assertTrue($bundleOptions[0]['required']); + $this->assertEquals('select', $bundleOptions[0]['type']); + $this->assertGreaterThan($oldOptionId, $bundleOptions[0]['option_id']); + $this->assertFalse(isset($bundleOptions[1])); + $this->assertEquals('simple2', $bundleOptions[0]['product_links'][0]['sku']); + $this->assertEquals(2, $bundleOptions[0]['product_links'][0]['qty']); + } + + /** + * Get the bundle_product_options custom attribute from product, null if the attribute is not set + * + * @param array $product + * @return array|null + */ + protected function getBundleProductOptions($product) + { + if (isset($product[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]["bundle_product_options"])) { + return $product[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]["bundle_product_options"]; + } else { + return null; + } + } + + /** + * Set the bundle_product_options custom attribute, replace existing attribute if exists + * + * @param array $product + * @param array $bundleProductOptions + */ + protected function setBundleProductOptions(&$product, $bundleProductOptions) + { + $product["extension_attributes"]["bundle_product_options"] = $bundleProductOptions; + return; + } + + /** + * Create dynamic bundle product with one option + * + * @return array + */ + protected function createDynamicBundleProduct() + { + $bundleProductOptions = [ + [ + "title" => "test option", + "type" => "checkbox", + "required" => 1, + "product_links" => [ + [ + "sku" => 'simple', + "qty" => 1, + "is_default" => true, + "price" => 10, + "price_type" => 1, + ], + ], + ], + ]; + + $uniqueId = self::BUNDLE_PRODUCT_ID; + $product = [ + "sku" => $uniqueId, + "name" => $uniqueId, + "type_id" => "bundle", + 'attribute_set_id' => 4, + "custom_attributes" => [ + "price_type" => [ + 'attribute_code' => 'price_type', + 'value' => \Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC + ], + "price_view" => [ + "attribute_code" => "price_view", + "value" => "1", + ], + ], + "extension_attributes" => [ + "bundle_product_options" => $bundleProductOptions, + ], + ]; + + $response = $this->createProduct($product); + $this->assertTrue( + isset($response[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]["bundle_product_options"]) + ); $resultBundleProductOptions = $response[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]["bundle_product_options"]; - $this->assertEquals($bundleProductOptions, $resultBundleProductOptions); + $this->assertTrue(isset($resultBundleProductOptions[0]["product_links"][0]["sku"])); $this->assertEquals('simple', $resultBundleProductOptions[0]["product_links"][0]["sku"]); + $this->assertTrue(isset($response['custom_attributes'])); + $customAttributes = $this->convertCustomAttributes($response['custom_attributes']); + $this->assertTrue(isset($customAttributes['price_type'])); + $this->assertEquals(\Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC, $customAttributes['price_type']); + $this->assertTrue(isset($customAttributes['price_view'])); + $this->assertEquals(1, $customAttributes['price_view']); + return $response; + } + + /** + * Create fixed price bundle product with one option + * + * @return array + */ + protected function createFixedPriceBundleProduct() + { + $bundleProductOptions = [ + [ + "title" => "test option", + "type" => "checkbox", + "required" => 1, + "product_links" => [ + [ + "sku" => 'simple', + "qty" => 1, + "price" => 20, + "price_type" => 1, + "is_default" => true, + ], + ], + ], + ]; + + $uniqueId = self::BUNDLE_PRODUCT_ID; + $product = [ + "sku" => $uniqueId, + "name" => $uniqueId, + "type_id" => "bundle", + "price" => 50, + 'attribute_set_id' => 4, + "custom_attributes" => [ + "price_type" => [ + 'attribute_code' => 'price_type', + 'value' => \Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED + ], + "price_view" => [ + "attribute_code" => "price_view", + "value" => "1", + ], + ], + "extension_attributes" => [ + "bundle_product_options" => $bundleProductOptions, + ], + ]; - $response = $this->getProduct($uniqueId); + $response = $this->createProduct($product); $resultBundleProductOptions = $response[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]["bundle_product_options"]; $this->assertEquals('simple', $resultBundleProductOptions[0]["product_links"][0]["sku"]); + $this->assertTrue(isset($response['custom_attributes'])); + $customAttributes = $this->convertCustomAttributes($response['custom_attributes']); + $this->assertTrue(isset($customAttributes['price_type'])); + $this->assertEquals(\Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED, $customAttributes['price_type']); + $this->assertTrue(isset($customAttributes['price_view'])); + $this->assertEquals(1, $customAttributes['price_view']); + return $response; + } + + protected function convertCustomAttributes($customAttributes) + { + $convertedCustomAttribute = []; + foreach ($customAttributes as $customAttribute) { + $convertedCustomAttribute[$customAttribute['attribute_code']] = $customAttribute['value']; + } + return $convertedCustomAttribute; } /** @@ -154,7 +469,56 @@ protected function createProduct($product) ]; $requestData = ['product' => $product]; $response = $this->_webApiCall($serviceInfo, $requestData); - $product[ProductInterface::SKU] = $response[ProductInterface::SKU]; - return $product; + return $response; + } + + /** + * Delete a product by sku + * + * @param $productSku + * @return bool + */ + protected function deleteProductBySku($productSku) + { + $resourcePath = self::RESOURCE_PATH . '/' . $productSku; + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => $resourcePath, + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'deleteById', + ], + ]; + $requestData = ["sku" => $productSku]; + $response = $this->_webApiCall($serviceInfo, $requestData); + return $response; + } + + /** + * Save product + * + * @param array $product + * @return array the created product data + */ + protected function saveProduct($product) + { + $resourcePath = self::RESOURCE_PATH . '/' . $product['sku']; + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => $resourcePath, + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Save', + ], + ]; + $requestData = ['product' => $product]; + $response = $this->_webApiCall($serviceInfo, $requestData); + return $response; } } diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Plugin/BundleSaveOptionsTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Plugin/BundleSaveOptionsTest.php new file mode 100644 index 0000000000000..b46a6544e18aa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Plugin/BundleSaveOptionsTest.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Bundle\Model\Plugin; + +class BundleSaveOptionsTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Catalog\Model\Product + */ + protected $_model; + + /** + * @var \Magento\Catalog\Api\ProductRepositoryInterface + */ + protected $productRepository; + + protected function setUp() + { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->productRepository = $objectManager->get('Magento\Catalog\Api\ProductRepositoryInterface'); + } + + /** + * @magentoDataFixture Magento/Bundle/_files/product.php + * @magentoDbIsolation enabled + */ + public function testSaveSuccess() + { + $title = "new title"; + $bundleProductSku = 'bundle-product'; + $product = $this->productRepository->get($bundleProductSku); + $bundleExtensionAttributes = $product->getExtensionAttributes()->getBundleProductOptions(); + $bundleOption = $bundleExtensionAttributes[0]; + $this->assertEquals(true, $bundleOption->getRequired()); + $bundleOption->setTitle($title); + + $oldDescription = $product->getDescription(); + $description = $oldDescription . "hello"; + $product->setDescription($description); + $product->getExtensionAttributes()->setBundleProductOptions([$bundleOption]); + $product = $this->productRepository->save($product); + + $this->assertEquals($description, $product->getDescription()); + $this->assertEquals($title, $product->getExtensionAttributes()->getBundleProductOptions()[0]->getTitle()); + } + + /** + * @magentoDataFixture Magento/Bundle/_files/product.php + * @magentoDbIsolation enabled + */ + public function testSaveFailure() + { + $this->markTestSkipped("When MAGETWO-36510 is fixed, need to change Dbisolation to disabled"); + $bundleProductSku = 'bundle-product'; + $product = $this->productRepository->get($bundleProductSku); + $bundleExtensionAttributes = $product->getExtensionAttributes()->getBundleProductOptions(); + $bundleOption = $bundleExtensionAttributes[0]; + $this->assertEquals(true, $bundleOption->getRequired()); + $bundleOption->setRequired(false); + //set an incorrect option id to trigger exception + $bundleOption->setOptionId(-1); + + $description = "hello"; + + $product->setDescription($description); + $product->getExtensionAttributes()->setBundleProductOptions([$bundleOption]); + $caughtException = false; + try { + $this->productRepository->save($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + $caughtException = true; + } + + $this->assertTrue($caughtException); + /** @var \Magento\Catalog\Model\Product $product */ + $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create('Magento\Catalog\Model\Product')->load($product->getId()); + $this->assertEquals(null, $product->getDescription()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/product.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/product.php index 1bb799689c7ec..663c2d0225742 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/_files/product.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/product.php @@ -31,6 +31,12 @@ \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED )->setStockData( ['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1] +)->setPriceView( + 1 +)->setPriceType( + 1 +)->setPrice( + 10.0 )->setBundleOptionsData( [ [ diff --git a/dev/tests/performance/benchmark.jmx b/dev/tests/performance/benchmark.jmx index cd9ed6d2ce32c..34b8617830f41 100644 --- a/dev/tests/performance/benchmark.jmx +++ b/dev/tests/performance/benchmark.jmx @@ -864,7 +864,7 @@ <collectionProp name="Arguments.arguments"> <elementProp name="billing[address_id]" elementType="HTTPArgument"> <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">37</stringProp> + <stringProp name="Argument.value"></stringProp> <stringProp name="Argument.metadata">=</stringProp> <boolProp name="HTTPArgument.use_equals">true</boolProp> <stringProp name="Argument.name">billing[address_id]</stringProp> @@ -962,7 +962,7 @@ </elementProp> <elementProp name="billing[save_in_address_book]" elementType="HTTPArgument"> <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">1</stringProp> + <stringProp name="Argument.value">0</stringProp> <stringProp name="Argument.metadata">=</stringProp> <boolProp name="HTTPArgument.use_equals">true</boolProp> <stringProp name="Argument.name">billing[save_in_address_book]</stringProp> diff --git a/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php b/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php index b90ff7acb3929..7b70fa5d35e61 100644 --- a/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php +++ b/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php @@ -205,6 +205,21 @@ public function unsetData($key = null) return parent::unsetData($key); } + /** + * Convert custom values if necessary + * + * @param array $customAttributes + * @return void + */ + protected function convertCustomAttributeValues(array &$customAttributes) + { + foreach ($customAttributes as $attributeCode => $attributeValue) { + if ($attributeValue instanceof \Magento\Framework\Api\AttributeValue) { + $customAttributes[$attributeCode] = $attributeValue->getValue(); + } + } + } + /** * Object data getter * @@ -231,6 +246,7 @@ public function getData($key = '', $index = null) $customAttributes = isset($this->_data[self::CUSTOM_ATTRIBUTES]) ? $this->_data[self::CUSTOM_ATTRIBUTES] : []; + $this->convertCustomAttributeValues($customAttributes); $data = array_merge($this->_data, $customAttributes); unset($data[self::CUSTOM_ATTRIBUTES]); } else { @@ -238,6 +254,9 @@ public function getData($key = '', $index = null) if ($data === null) { /** Try to find necessary data in custom attributes */ $data = parent::getData(self::CUSTOM_ATTRIBUTES . "/{$key}", $index); + if ($data instanceof \Magento\Framework\Api\AttributeValue) { + $data = $data->getValue(); + } } } return $data; diff --git a/lib/internal/Magento/Framework/Webapi/ServiceInputProcessor.php b/lib/internal/Magento/Framework/Webapi/ServiceInputProcessor.php index 14caad463c95a..e232546d6ac1e 100644 --- a/lib/internal/Magento/Framework/Webapi/ServiceInputProcessor.php +++ b/lib/internal/Magento/Framework/Webapi/ServiceInputProcessor.php @@ -137,8 +137,7 @@ protected function _createFromArray($className, $data) if (is_subclass_of($className, self::EXTENSION_ATTRIBUTES_TYPE)) { $className = substr($className, 0, -strlen('Interface')); } - $factory = $this->objectManager->get($className . 'Factory'); - $object = $factory->create(); + $object = $this->objectManager->create($className); foreach ($data as $propertyName => $value) { // Converts snake_case to uppercase CamelCase to help form getter/setter method names diff --git a/lib/internal/Magento/Framework/Webapi/Test/Unit/ServiceInputProcessorTest.php b/lib/internal/Magento/Framework/Webapi/Test/Unit/ServiceInputProcessorTest.php index 41cdf8184ba37..ac75d2c131fb1 100644 --- a/lib/internal/Magento/Framework/Webapi/Test/Unit/ServiceInputProcessorTest.php +++ b/lib/internal/Magento/Framework/Webapi/Test/Unit/ServiceInputProcessorTest.php @@ -42,6 +42,14 @@ public function setUp() $this->objectManagerMock = $this->getMockBuilder('\Magento\Framework\ObjectManagerInterface') ->disableOriginalConstructor() ->getMock(); + $this->objectManagerMock->expects($this->any()) + ->method('create') + ->willReturnCallback( + function ($className) use ($objectManager) { + return $objectManager->getObject($className); + } + ); + /** @var \Magento\Framework\Reflection\TypeProcessor $typeProcessor */ $typeProcessor = $objectManager->getObject('Magento\Framework\Reflection\TypeProcessor'); $cache = $this->getMockBuilder('Magento\Framework\App\Cache\Type\Webapi') @@ -119,12 +127,6 @@ public function testNonExistentPropertiesWithoutDefaultArgumentValue() public function testNestedDataProperties() { - $this->setupFactory( - [ - 'Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\Nested', - '\Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\Simple', - ] - ); $data = ['nested' => ['details' => ['entityId' => 15, 'name' => 'Test']]]; $result = $this->serviceInputProcessor->process( 'Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\TestService', @@ -167,7 +169,6 @@ public function testSimpleArrayProperties() public function testAssociativeArrayProperties() { - $this->setupFactory(['Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\Simple']); $data = ['associativeArray' => ['key' => 'value', 'key_two' => 'value_two']]; $result = $this->serviceInputProcessor->process( 'Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\TestService', @@ -186,7 +187,6 @@ public function testAssociativeArrayProperties() public function testAssociativeArrayPropertiesWithItem() { - $this->setupFactory(['Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\AssociativeArray']); $data = ['associativeArray' => ['item' => 'value']]; $result = $this->serviceInputProcessor->process( 'Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\TestService', @@ -204,7 +204,6 @@ public function testAssociativeArrayPropertiesWithItem() public function testAssociativeArrayPropertiesWithItemArray() { - $this->setupFactory(['Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\AssociativeArray']); $data = ['associativeArray' => ['item' => ['value1','value2']]]; $result = $this->serviceInputProcessor->process( 'Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\TestService', @@ -223,11 +222,6 @@ public function testAssociativeArrayPropertiesWithItemArray() public function testArrayOfDataObjectProperties() { - $this->setupFactory( - [ - '\Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\Simple' - ] - ); $data = [ 'dataObjects' => [ ['entityId' => 14, 'name' => 'First'], @@ -259,7 +253,6 @@ public function testArrayOfDataObjectProperties() public function testNestedSimpleArrayProperties() { - $this->setupFactory(['Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\SimpleArray']); $data = ['arrayData' => ['ids' => [1, 2, 3, 4]]]; $result = $this->serviceInputProcessor->process( 'Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\TestService', @@ -281,7 +274,6 @@ public function testNestedSimpleArrayProperties() public function testNestedAssociativeArrayProperties() { - $this->setupFactory(['Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\AssociativeArray']); $data = [ 'associativeArrayData' => ['associativeArray' => ['key' => 'value', 'key2' => 'value2']], ]; @@ -305,12 +297,6 @@ public function testNestedAssociativeArrayProperties() public function testNestedArrayOfDataObjectProperties() { - $this->setupFactory( - [ - 'Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\DataArray', - '\Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\Simple', - ] - ); $data = [ 'dataObjects' => [ 'items' => [['entityId' => 1, 'name' => 'First'], ['entityId' => 2, 'name' => 'Second']], @@ -352,14 +338,6 @@ public function testNestedArrayOfDataObjectProperties() */ public function testCustomAttributesProperties($customAttributeType, $inputData, $expectedObject) { - $this->setupFactory( - [ - 'Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\ObjectWithCustomAttributes', - '\Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\Simple', - 'Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\Simple', - 'Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\SimpleArray', - ] - ); $this->customAttributeTypeLocator->expects($this->any())->method('getType')->willReturn($customAttributeType); $result = $this->serviceInputProcessor->process( @@ -521,27 +499,4 @@ protected function getObjectWithCustomAttributes($type, $value = []) ]] ); } - protected function setupFactory(array $classNames) - { - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - - $returnValueMap = []; - foreach ($classNames as $className) { - $factoryMock = $this->getMockBuilder($className . 'Factory') - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); - $factoryMock->expects($this->any()) - ->method('create') - ->willReturnCallback( - function () use ($objectManager, $className) { - return $objectManager->getObject($className); - } - ); - $returnValueMap[] = [$className . 'Factory', $factoryMock]; - } - $this->objectManagerMock->expects($this->any()) - ->method('get') - ->will($this->returnValueMap($returnValueMap)); - } }