Skip to content

Commit 5f735a2

Browse files
Merge branch 'magento-commerce:2.4-develop' into L3-PR-2024-02-16
2 parents 63b9a67 + b2286ec commit 5f735a2

File tree

25 files changed

+561
-135
lines changed

25 files changed

+561
-135
lines changed

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

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -84,19 +84,29 @@ public function __construct(
8484
* Bulk options save
8585
*
8686
* @param ProductInterface $bundleProduct
87-
* @param OptionInterface[] $options
87+
* @param array $options
88+
* @param array $existingBundleProductOptions
8889
* @return void
8990
* @throws CouldNotSaveException
90-
* @throws NoSuchEntityException
9191
* @throws InputException
92+
* @throws NoSuchEntityException
9293
*/
93-
public function saveBulk(ProductInterface $bundleProduct, array $options): void
94-
{
94+
public function saveBulk(
95+
ProductInterface $bundleProduct,
96+
array $options,
97+
array $existingBundleProductOptions = []
98+
): void {
9599
$metadata = $this->metadataPool->getMetadata(ProductInterface::class);
96100
$optionCollection = $this->type->getOptionsCollection($bundleProduct);
97101

98102
foreach ($options as $option) {
99-
$this->saveOptionItem($bundleProduct, $option, $optionCollection, $metadata);
103+
$this->saveOptionItem(
104+
$bundleProduct,
105+
$option,
106+
$optionCollection,
107+
$metadata,
108+
$existingBundleProductOptions
109+
);
100110
}
101111

102112
$bundleProduct->setIsRelationsChanged(true);
@@ -109,42 +119,42 @@ public function saveBulk(ProductInterface $bundleProduct, array $options): void
109119
* @param OptionInterface $option
110120
* @param Collection $optionCollection
111121
* @param EntityMetadataInterface $metadata
122+
* @param array $existingBundleProductOptions
112123
* @return void
113124
* @throws CouldNotSaveException
114-
* @throws NoSuchEntityException
115125
* @throws InputException
126+
* @throws NoSuchEntityException
116127
*/
117128
private function saveOptionItem(
118129
ProductInterface $bundleProduct,
119130
OptionInterface $option,
120131
Collection $optionCollection,
121-
EntityMetadataInterface $metadata
132+
EntityMetadataInterface $metadata,
133+
array $existingBundleProductOptions = []
122134
) : void {
123135
$linksToAdd = [];
124136

125137
$option->setStoreId($bundleProduct->getStoreId());
126138
$parentId = $bundleProduct->getData($metadata->getLinkField());
127139
$option->setParentId($parentId);
128140
$optionId = $option->getOptionId();
141+
$existingOption = $this->retrieveExistingOption($optionCollection, $option, $existingBundleProductOptions);
129142

130-
/** @var \Magento\Bundle\Model\Option $existingOption */
131-
$existingOption = $optionCollection->getItemById($option->getOptionId())
132-
?? $optionCollection->getNewEmptyItem();
133143
if (!$optionId || $existingOption->getParentId() != $parentId) {
134144
$option->setOptionId(null);
135145
$option->setDefaultTitle($option->getTitle());
136146
if (is_array($option->getProductLinks())) {
137147
$linksToAdd = $option->getProductLinks();
138148
}
139149
} else {
140-
if (!$existingOption->getOptionId()) {
150+
if (!$existingOption || !$existingOption->getOptionId()) {
141151
throw new NoSuchEntityException(
142152
__("The option that was requested doesn't exist. Verify the entity and try again.")
143153
);
144154
}
145155

146156
$option->setData(array_merge($existingOption->getData(), $option->getData()));
147-
$this->updateOptionSelection($bundleProduct, $option);
157+
$this->updateOptionSelection($bundleProduct, $option, $existingOption);
148158
}
149159

150160
try {
@@ -183,15 +193,21 @@ public function save(ProductInterface $bundleProduct, OptionInterface $option)
183193
*
184194
* @param ProductInterface $product
185195
* @param OptionInterface $option
196+
* @param OptionInterface|null $existingOption
186197
* @return void
198+
* @throws CouldNotSaveException
199+
* @throws InputException
200+
* @throws NoSuchEntityException
187201
*/
188-
private function updateOptionSelection(ProductInterface $product, OptionInterface $option)
189-
{
190-
$optionId = $option->getOptionId();
191-
$existingLinks = $this->linkManagement->getChildren($product->getSku(), $optionId);
202+
private function updateOptionSelection(
203+
ProductInterface $product,
204+
OptionInterface $option,
205+
?OptionInterface $existingOption = null
206+
):void {
192207
$linksToAdd = [];
193208
$linksToUpdate = [];
194209
$linksToDelete = [];
210+
195211
if (is_array($option->getProductLinks())) {
196212
$productLinks = $option->getProductLinks();
197213
foreach ($productLinks as $productLink) {
@@ -201,20 +217,24 @@ private function updateOptionSelection(ProductInterface $product, OptionInterfac
201217
$linksToUpdate[] = $productLink;
202218
}
203219
}
204-
/** @var LinkInterface[] $linksToDelete */
205-
$linksToDelete = $this->compareLinks($existingLinks, $linksToUpdate);
206-
$linksToUpdate = $this->verifyLinksToUpdate($existingLinks, $linksToUpdate);
220+
if (!empty($existingOption) && !empty($existingOption->getProductLinks())) {
221+
$linksToDelete = $this->compareLinks($existingOption->getProductLinks(), $linksToUpdate);
222+
$linksToUpdate = $this->verifyLinksToUpdate($existingOption->getProductLinks(), $linksToUpdate);
223+
}
207224
}
225+
208226
foreach ($linksToUpdate as $linkedProduct) {
209227
$this->linkManagement->saveChild($product->getSku(), $linkedProduct);
210228
}
229+
211230
foreach ($linksToDelete as $linkedProduct) {
212231
$this->linkManagement->removeChild(
213232
$product->getSku(),
214233
$option->getOptionId(),
215234
$linkedProduct->getSku()
216235
);
217236
}
237+
218238
$this->addChildren->addChildren($product, (int)$option->getOptionId(), $linksToAdd);
219239
}
220240

@@ -300,4 +320,42 @@ private function compareLinks(array $firstArray, array $secondArray)
300320

301321
return $result;
302322
}
323+
324+
/**
325+
* Retrieve option from list.
326+
*
327+
* @param Collection $optionCollection
328+
* @param OptionInterface $option
329+
* @param array $existingBundleProductOptions
330+
* @return OptionInterface
331+
*/
332+
private function retrieveExistingOption(
333+
Collection $optionCollection,
334+
OptionInterface $option,
335+
array $existingBundleProductOptions
336+
): OptionInterface {
337+
$existingOption = $optionCollection->getItemById($option->getOptionId());
338+
339+
$incomingOption = current(
340+
array_filter($existingBundleProductOptions, function ($obj) use ($option) {
341+
return $obj->getData()['option_id'] == $option->getId();
342+
})
343+
);
344+
345+
if (!empty($incomingOption)) {
346+
$existingOption->setData(
347+
array_merge(
348+
$existingOption->getData(),
349+
$incomingOption->getData()
350+
)
351+
);
352+
}
353+
354+
// @phpstan-ignore-next-line
355+
if (empty($existingOption)) {
356+
$existingOption = $optionCollection->getNewEmptyItem();
357+
}
358+
359+
return $existingOption;
360+
}
303361
}

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

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
/**
2323
* Bundle product save handler
24+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
2425
*/
2526
class SaveHandler implements ExtensionInterface
2627
{
@@ -107,8 +108,7 @@ public function execute($entity, $arguments = [])
107108

108109
if (!$entity->getCopyFromView()) {
109110
$this->processRemovedOptions($entity, $existingOptionsIds, $optionIds);
110-
$newOptionsIds = array_diff($optionIds, $existingOptionsIds);
111-
$this->saveOptions($entity, $bundleProductOptions, $newOptionsIds);
111+
$this->saveOptions($entity, $bundleProductOptions, $existingBundleProductOptions);
112112
} else {
113113
//save only labels and not selections + product links
114114
$this->saveOptions($entity, $bundleProductOptions);
@@ -150,20 +150,20 @@ protected function removeOptionLinks($entitySku, $option)
150150
/**
151151
* Perform save for all options entities.
152152
*
153-
* @param object $entity
153+
* @param ProductInterface $entity
154154
* @param array $options
155-
* @param array $newOptionsIds
156-
*
155+
* @param array $existingBundleProductOptions
157156
* @return void
157+
* @throws InputException
158+
* @throws NoSuchEntityException
159+
* @throws \Magento\Framework\Exception\CouldNotSaveException
158160
*/
159-
private function saveOptions($entity, array $options, array $newOptionsIds = []): void
160-
{
161-
foreach ($options as $option) {
162-
if (in_array($option->getOptionId(), $newOptionsIds)) {
163-
$option->setOptionId(null);
164-
}
165-
}
166-
$this->optionSave->saveBulk($entity, $options);
161+
private function saveOptions(
162+
ProductInterface $entity,
163+
array $options,
164+
array $existingBundleProductOptions = []
165+
): void {
166+
$this->optionSave->saveBulk($entity, $options, $existingBundleProductOptions);
167167
}
168168

169169
/**

app/code/Magento/Bundle/Pricing/Price/DiscountCalculator.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,21 @@
77
namespace Magento\Bundle\Pricing\Price;
88

99
use Magento\Catalog\Model\Product;
10+
use Magento\Framework\Pricing\PriceCurrencyInterface;
1011

1112
/**
12-
* Class DiscountCalculator
13+
* Check the product available discount and apply the correct discount to the price
1314
*/
1415
class DiscountCalculator
1516
{
17+
18+
/**
19+
* @param PriceCurrencyInterface $priceCurrency
20+
*/
21+
public function __construct(private readonly PriceCurrencyInterface $priceCurrency)
22+
{
23+
}
24+
1625
/**
1726
* Apply percentage discount
1827
*
@@ -32,6 +41,7 @@ public function calculateDiscount(Product $product, $value = null)
3241
$discount = min($price->getDiscountPercent(), $discount ?: $price->getDiscountPercent());
3342
}
3443
}
35-
return (null !== $discount) ? $discount/100 * $value : $value;
44+
return (null !== $discount) ?
45+
$this->priceCurrency->roundPrice($discount/100 * $value, 2) : $value;
3646
}
3747
}

app/code/Magento/Bundle/Test/Unit/Model/Option/SaveActionTest.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,6 @@ public function testSaveBulk()
129129
->method('getMetadata')
130130
->willReturn($metadata);
131131

132-
$this->linkManagement->expects($this->once())
133-
->method('getChildren')
134-
->willReturn([]);
135132
$this->product->expects($this->once())
136133
->method('setIsRelationsChanged')
137134
->with(true);

app/code/Magento/Bundle/Test/Unit/Pricing/Price/DiscountCalculatorTest.php

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
use Magento\Bundle\Pricing\Price\DiscountProviderInterface;
1212
use Magento\Catalog\Model\Product;
1313
use Magento\Catalog\Pricing\Price\FinalPrice;
14+
use Magento\Framework\Pricing\PriceCurrencyInterface;
1415
use Magento\Framework\Pricing\PriceInfo\Base;
16+
use Magento\Framework\Pricing\Price\PriceInterface;
1517
use PHPUnit\Framework\MockObject\MockObject;
1618
use PHPUnit\Framework\TestCase;
1719

@@ -42,21 +44,31 @@ class DiscountCalculatorTest extends TestCase
4244
*/
4345
protected $priceMock;
4446

47+
/**
48+
* @var PriceCurrencyInterface|MockObject
49+
*/
50+
private $priceCurrencyMock;
51+
4552
/**
4653
* Test setUp
4754
*/
4855
protected function setUp(): void
4956
{
5057
$this->productMock = $this->createMock(Product::class);
51-
$this->priceInfoMock = $this->createPartialMock(
52-
Base::class,
53-
['getPrice', 'getPrices']
54-
);
58+
$this->priceInfoMock = $this->getMockBuilder(Base::class)
59+
->disableOriginalConstructor()
60+
->onlyMethods(['getPrice', 'getPrices'])
61+
->addMethods(['getValue'])
62+
->getMock();
5563
$this->finalPriceMock = $this->createMock(FinalPrice::class);
5664
$this->priceMock = $this->getMockForAbstractClass(
5765
DiscountProviderInterface::class
5866
);
59-
$this->calculator = new DiscountCalculator();
67+
$this->priceCurrencyMock = $this->getMockBuilder(PriceCurrencyInterface::class)
68+
->disableOriginalConstructor()
69+
->addMethods(['roundPrice'])
70+
->getMockForAbstractClass();
71+
$this->calculator = new DiscountCalculator($this->priceCurrencyMock);
6072
}
6173

6274
/**
@@ -98,26 +110,52 @@ public function testCalculateDiscountWithDefaultAmount()
98110
$this->getPriceMock(40),
99111
]
100112
);
113+
$this->priceCurrencyMock->expects($this->once())
114+
->method('roundPrice')
115+
->willReturn(20);
101116
$this->assertEquals(20, $this->calculator->calculateDiscount($this->productMock));
102117
}
103118

104119
/**
105120
* test method calculateDiscount with custom price amount
121+
*
122+
* @dataProvider providerForWithDifferentAmount
106123
*/
107-
public function testCalculateDiscountWithCustomAmount()
124+
public function testCalculateDiscountWithCustomAmount(mixed $discount, mixed $value, float $expectedResult)
108125
{
109-
$this->productMock->expects($this->once())
126+
$this->productMock->expects($this->any())
110127
->method('getPriceInfo')
111128
->willReturn($this->priceInfoMock);
112-
$this->priceInfoMock->expects($this->once())
129+
$this->priceInfoMock->expects($this->any())
113130
->method('getPrices')
114-
->willReturn(
115-
[
116-
$this->getPriceMock(30),
117-
$this->getPriceMock(20),
118-
$this->getPriceMock(40),
119-
]
131+
->willReturn([$this->getPriceMock($discount)]);
132+
if ($value === null) {
133+
$abstractPriceMock = $this->getMockForAbstractClass(
134+
PriceInterface::class
120135
);
121-
$this->assertEquals(10, $this->calculator->calculateDiscount($this->productMock, 50));
136+
$this->priceInfoMock->expects($this->any())
137+
->method('getPrice')
138+
->willReturn($abstractPriceMock);
139+
$abstractPriceMock->expects($this->any())
140+
->method('getValue')
141+
->willReturn($expectedResult);
142+
}
143+
$this->priceCurrencyMock->expects($this->any())
144+
->method('roundPrice')
145+
->willReturn($expectedResult);
146+
$this->assertEquals($expectedResult, $this->calculator->calculateDiscount($this->productMock, $value));
147+
}
148+
149+
/**
150+
* @return array
151+
*/
152+
public function providerForWithDifferentAmount()
153+
{
154+
return [
155+
'test case 1 with discount amount' => [20, 50, 10],
156+
'test case 2 for null discount amount' => [null, 30, 30],
157+
'test case 3 with discount amount' => [99, 5.5, 5.45],
158+
'test case 4 with null value' => [50, null, 50]
159+
];
122160
}
123161
}

0 commit comments

Comments
 (0)