Skip to content

Commit f6f3622

Browse files
committed
ACP2E-4212: Bundle product with scheduled updates removes the bundle items option on product save action
- Fix bundle product relation is removed when duplicate product option is removed - Fix re-saving original bundle product after duplicate in the same runtime removes selections in the original product - Add test coverage
1 parent 23aea05 commit f6f3622

File tree

7 files changed

+267
-225
lines changed

7 files changed

+267
-225
lines changed

app/code/Magento/Bundle/Model/LinkManagement.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,14 +453,16 @@ public function removeChild($sku, $optionId, $childSku)
453453
$excludeSelectionIds = [];
454454
$usedProductIds = [];
455455
$removeSelectionIds = [];
456+
$removeProductIds = [];
456457
foreach ($this->getOptions($product) as $option) {
457458
/** @var Selection $selection */
458459
foreach ($option->getSelections() as $selection) {
459460
if ((strcasecmp($selection->getSku(), $childSku) == 0) && ($selection->getOptionId() == $optionId)) {
460461
$removeSelectionIds[] = $selection->getSelectionId();
461-
$usedProductIds[] = $selection->getProductId();
462+
$removeProductIds[] = $selection->getProductId();
462463
continue;
463464
}
465+
$usedProductIds[] = $selection->getProductId();
464466
$excludeSelectionIds[] = $selection->getSelectionId();
465467
}
466468
}
@@ -473,7 +475,10 @@ public function removeChild($sku, $optionId, $childSku)
473475
/* @var $resource Bundle */
474476
$resource = $this->bundleFactory->create();
475477
$resource->dropAllUnneededSelections($product->getData($linkField), $excludeSelectionIds);
476-
$resource->removeProductRelations($product->getData($linkField), array_unique($usedProductIds));
478+
$productRelationsToRemove = array_diff($removeProductIds, $usedProductIds);
479+
if ($productRelationsToRemove) {
480+
$resource->removeProductRelations($product->getData($linkField), array_unique($productRelationsToRemove));
481+
}
477482

478483
return true;
479484
}

app/code/Magento/Bundle/Model/Option/SaveAction.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,6 @@ private function saveOptionItem(
165165

166166
/** @var LinkInterface $linkedProduct */
167167
foreach ($linksToAdd as $linkedProduct) {
168-
$linkedProduct->setId(null);
169-
$linkedProduct->setSelectionId(null);
170168
$this->linkManagement->addChild($bundleProduct, $option->getOptionId(), $linkedProduct);
171169
}
172170
}

app/code/Magento/Bundle/Model/Product/CopyConstructor/Bundle.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,15 @@ public function build(Product $product, Product $duplicate)
3535
* Set option and selection ids to 'null' in order to create new option(selection) for duplicated product,
3636
* but not modifying existing one, which led to lost of option(selection) in original product.
3737
*/
38-
$productLinks = $duplicatedBundleOption->getProductLinks() ?: [];
39-
foreach ($productLinks as $productLink) {
40-
$productLink->setSelectionId(null);
38+
$productLinks = [];
39+
foreach ($duplicatedBundleOption->getProductLinks() ?: [] as $productLink) {
40+
$productLinkDuplicate = clone $productLink;
41+
$productLinkDuplicate->setId(null);
42+
$productLinkDuplicate->setSelectionId(null);
43+
$productLinkDuplicate->setOptionId(null);
44+
$productLinks[] = $productLinkDuplicate;
4145
}
46+
$duplicatedBundleOption->setProductLinks($productLinks);
4247
$duplicatedBundleOption->setOptionId(null);
4348
$duplicatedBundleOptions[$key] = $duplicatedBundleOption;
4449
}

app/code/Magento/Bundle/Model/Product/SaveHandler.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,7 @@ protected function removeOptionLinks($entitySku, $option)
141141
$links = $option->getProductLinks();
142142
if (!empty($links)) {
143143
foreach ($links as $link) {
144-
$linkCanBeDeleted = $this->checkOptionLinkIfExist->execute($entitySku, $option, $link);
145-
if ($linkCanBeDeleted) {
146-
$this->productLinkManagement->removeChild($entitySku, $option->getId(), $link->getSku());
147-
}
144+
$this->productLinkManagement->removeChild($entitySku, $option->getId(), $link->getSku());
148145
}
149146
}
150147
}

app/code/Magento/Bundle/Test/Unit/Model/LinkManagementTest.php

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -641,14 +641,9 @@ public function testAddChild(): void
641641
{
642642
$selectionId = 42;
643643
$optionId = 1;
644-
$productLink = $this->getMockBuilder(LinkInterface::class)
645-
->onlyMethods(['getSku', 'getOptionId', 'setOptionId', 'setId'])
646-
->addMethods(['getSelectionId', 'setSelectionId'])
647-
->disableOriginalConstructor()
648-
->getMockForAbstractClass();
649-
$productLink->method('getSku')->willReturn('linked_product_sku');
650-
$productLink->method('getOptionId')->willReturn(1);
651-
$productLink->method('getSelectionId')->willReturn(1);
644+
$sku = 'linked_product_sku';
645+
$productLink = $this->createPartialMock(\Magento\Bundle\Model\Link::class, []);
646+
$productLink->setSku($sku);
652647

653648
$this->metadataMock->expects($this->exactly(2))->method('getLinkField')->willReturn($this->linkField);
654649
$productMock = $this->createMock(Product::class);
@@ -664,7 +659,7 @@ public function testAddChild(): void
664659
$this->productRepository
665660
->expects($this->once())
666661
->method('get')
667-
->with('linked_product_sku')
662+
->with($sku)
668663
->willReturn($linkedProductMock);
669664

670665
$store = $this->createMock(Store::class);
@@ -702,12 +697,12 @@ public function testAddChild(): void
702697
$selection->expects($this->once())->method('save');
703698
$selection->expects($this->any())->method('getId')->willReturn($selectionId);
704699
$this->bundleSelectionMock->expects($this->once())->method('create')->willReturn($selection);
705-
$productLink->expects($this->once())->method('setSelectionId')->with($selectionId)->willReturnSelf();
706-
$productLink->expects($this->once())->method('setId')->with($selectionId)->willReturnSelf();
707-
$productLink->expects($this->once())->method('setOptionId')->with($optionId)->willReturnSelf();
708700

709701
$result = $this->model->addChild($productMock, $optionId, $productLink);
710702
$this->assertEquals($selectionId, $result);
703+
$this->assertEquals($selectionId, $productLink->getId());
704+
$this->assertEquals($selectionId, $productLink->getSelectionId());
705+
$this->assertEquals($optionId, $productLink->getOptionId());
711706
}
712707

713708
/**

app/code/Magento/Bundle/Test/Unit/Model/Product/CopyConstructor/BundleTest.php

Lines changed: 82 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
namespace Magento\Bundle\Test\Unit\Model\Product\CopyConstructor;
99

10-
use Magento\Bundle\Api\Data\BundleOptionInterface;
1110
use Magento\Bundle\Model\Link;
11+
use Magento\Bundle\Model\Option;
1212
use Magento\Bundle\Model\Product\CopyConstructor\Bundle;
1313
use Magento\Catalog\Api\Data\ProductExtensionInterface;
1414
use Magento\Catalog\Model\Product;
@@ -65,38 +65,39 @@ public function testBuildPositive()
6565
$product->expects($this->once())
6666
->method('getExtensionAttributes')
6767
->willReturn($extensionAttributesProduct);
68-
69-
$productLink = $this->getMockBuilder(Link::class)
70-
->addMethods(['setSelectionId'])
71-
->disableOriginalConstructor()
72-
->getMock();
73-
$productLink->expects($this->exactly(2))
74-
->method('setSelectionId')
75-
->with($this->identicalTo(null));
76-
$firstOption = $this->getMockBuilder(BundleOptionInterface::class)
77-
->addMethods(['getProductLinks'])
78-
->disableOriginalConstructor()
79-
->getMockForAbstractClass();
80-
$firstOption->expects($this->once())
81-
->method('getProductLinks')
82-
->willReturn([$productLink]);
83-
$firstOption->expects($this->once())
84-
->method('setOptionId')
85-
->with($this->identicalTo(null));
86-
$secondOption = $this->getMockBuilder(BundleOptionInterface::class)
87-
->addMethods(['getProductLinks'])
88-
->disableOriginalConstructor()
89-
->getMockForAbstractClass();
90-
$secondOption->expects($this->once())
91-
->method('getProductLinks')
92-
->willReturn([$productLink]);
93-
$secondOption->expects($this->once())
94-
->method('setOptionId')
95-
->with($this->identicalTo(null));
96-
$bundleOptions = [
97-
$firstOption,
98-
$secondOption
68+
69+
$bundleOptionsData = [
70+
[
71+
'option_id' => 1,
72+
'title' => 'Option 1',
73+
'product_links' => [
74+
[
75+
'option_id' => 1,
76+
'id' => 1,
77+
'selection_id' => 1,
78+
'sku' => 'sku-1'
79+
],
80+
]
81+
],
82+
[
83+
'option_id' => 2,
84+
'title' => 'Option 2',
85+
'product_links' => [
86+
[
87+
'option_id' => 2,
88+
'id' => 2,
89+
'selection_id' => 2,
90+
'sku' => 'sku-2'
91+
]
92+
]
93+
]
9994
];
95+
$bundleOptions = array_map(
96+
fn ($optionData) => $this->createOptionMock(
97+
[...$optionData, 'product_links' => array_map($this->createLinkMock(...), $optionData['product_links'])]
98+
),
99+
$bundleOptionsData
100+
);
100101
$extensionAttributesProduct->expects($this->once())
101102
->method('getBundleProductOptions')
102103
->willReturn($bundleOptions);
@@ -115,13 +116,57 @@ public function testBuildPositive()
115116
->willReturn($extensionAttributesDuplicate);
116117
$extensionAttributesDuplicate->expects($this->once())
117118
->method('setBundleProductOptions')
118-
->willReturnCallback(function ($bundleOptions) {
119-
if ($bundleOptions) {
120-
return null;
121-
}
122-
});
119+
->with(
120+
$this->callback(function ($options) use (&$bundleOptionsClone) {
121+
$bundleOptionsClone = $options;
122+
return !empty($bundleOptionsClone);
123+
})
124+
);
123125

124126
$this->model->build($product, $duplicate);
127+
foreach ($bundleOptionsData as $key => $optionData) {
128+
$bundleOption = $bundleOptions[$key];
129+
$bundleOptionClone = $bundleOptionsClone[$key];
130+
131+
$this->assertEquals($optionData['option_id'], $bundleOption->getOptionId());
132+
$this->assertEquals($optionData['title'], $bundleOption->getTitle());
133+
134+
$this->assertNotEquals($bundleOption, $bundleOptionClone);
135+
136+
$this->assertNull($bundleOptionClone->getOptionId());
137+
$this->assertEquals($optionData['title'], $bundleOptionClone->getTitle());
138+
139+
foreach ($optionData['product_links'] as $productLinkKey => $productLinkData) {
140+
$productLink = $bundleOption->getProductLinks()[$productLinkKey];
141+
$productLinkClone = $bundleOptionClone->getProductLinks()[$productLinkKey];
142+
143+
$this->assertEquals($productLinkData['option_id'], $productLink->getOptionId());
144+
$this->assertEquals($productLinkData['id'], $productLink->getId());
145+
$this->assertEquals($productLinkData['selection_id'], $productLink->getSelectionId());
146+
$this->assertEquals($productLinkData['sku'], $productLink->getSku());
147+
148+
$this->assertNotEquals($productLink, $productLinkClone);
149+
150+
$this->assertNull($productLinkClone->getId());
151+
$this->assertNull($productLinkClone->getOptionId());
152+
$this->assertNull($productLinkClone->getSelectionId());
153+
$this->assertEquals($productLinkData['sku'], $productLinkClone->getSku());
154+
}
155+
}
156+
}
157+
158+
private function createOptionMock(array $data): Option
159+
{
160+
$option = $this->createPartialMock(Option::class, []);
161+
$option->addData($data);
162+
return $option;
163+
}
164+
165+
private function createLinkMock(array $data): Link
166+
{
167+
$productLink = $this->createPartialMock(Link::class, []);
168+
$productLink->addData($data);
169+
return $productLink;
125170
}
126171

127172
/**

0 commit comments

Comments
 (0)