Skip to content

Commit

Permalink
Merge branch '2.4-develop' of https://github.com/mage-os/mirror-magento2
Browse files Browse the repository at this point in the history
 into 2.4-develop
  • Loading branch information
mage-os-ci committed Mar 16, 2024
2 parents 81690ae + b2286ec commit afa06bd
Show file tree
Hide file tree
Showing 53 changed files with 1,484 additions and 153 deletions.
96 changes: 77 additions & 19 deletions app/code/Magento/Bundle/Model/Option/SaveAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,29 @@ public function __construct(
* Bulk options save
*
* @param ProductInterface $bundleProduct
* @param OptionInterface[] $options
* @param array $options
* @param array $existingBundleProductOptions
* @return void
* @throws CouldNotSaveException
* @throws NoSuchEntityException
* @throws InputException
* @throws NoSuchEntityException
*/
public function saveBulk(ProductInterface $bundleProduct, array $options): void
{
public function saveBulk(
ProductInterface $bundleProduct,
array $options,
array $existingBundleProductOptions = []
): void {
$metadata = $this->metadataPool->getMetadata(ProductInterface::class);
$optionCollection = $this->type->getOptionsCollection($bundleProduct);

foreach ($options as $option) {
$this->saveOptionItem($bundleProduct, $option, $optionCollection, $metadata);
$this->saveOptionItem(
$bundleProduct,
$option,
$optionCollection,
$metadata,
$existingBundleProductOptions
);
}

$bundleProduct->setIsRelationsChanged(true);
Expand All @@ -109,42 +119,42 @@ public function saveBulk(ProductInterface $bundleProduct, array $options): void
* @param OptionInterface $option
* @param Collection $optionCollection
* @param EntityMetadataInterface $metadata
* @param array $existingBundleProductOptions
* @return void
* @throws CouldNotSaveException
* @throws NoSuchEntityException
* @throws InputException
* @throws NoSuchEntityException
*/
private function saveOptionItem(
ProductInterface $bundleProduct,
OptionInterface $option,
Collection $optionCollection,
EntityMetadataInterface $metadata
EntityMetadataInterface $metadata,
array $existingBundleProductOptions = []
) : void {
$linksToAdd = [];

$option->setStoreId($bundleProduct->getStoreId());
$parentId = $bundleProduct->getData($metadata->getLinkField());
$option->setParentId($parentId);
$optionId = $option->getOptionId();
$existingOption = $this->retrieveExistingOption($optionCollection, $option, $existingBundleProductOptions);

/** @var \Magento\Bundle\Model\Option $existingOption */
$existingOption = $optionCollection->getItemById($option->getOptionId())
?? $optionCollection->getNewEmptyItem();
if (!$optionId || $existingOption->getParentId() != $parentId) {
$option->setOptionId(null);
$option->setDefaultTitle($option->getTitle());
if (is_array($option->getProductLinks())) {
$linksToAdd = $option->getProductLinks();
}
} else {
if (!$existingOption->getOptionId()) {
if (!$existingOption || !$existingOption->getOptionId()) {
throw new NoSuchEntityException(
__("The option that was requested doesn't exist. Verify the entity and try again.")
);
}

$option->setData(array_merge($existingOption->getData(), $option->getData()));
$this->updateOptionSelection($bundleProduct, $option);
$this->updateOptionSelection($bundleProduct, $option, $existingOption);
}

try {
Expand Down Expand Up @@ -183,15 +193,21 @@ public function save(ProductInterface $bundleProduct, OptionInterface $option)
*
* @param ProductInterface $product
* @param OptionInterface $option
* @param OptionInterface|null $existingOption
* @return void
* @throws CouldNotSaveException
* @throws InputException
* @throws NoSuchEntityException
*/
private function updateOptionSelection(ProductInterface $product, OptionInterface $option)
{
$optionId = $option->getOptionId();
$existingLinks = $this->linkManagement->getChildren($product->getSku(), $optionId);
private function updateOptionSelection(
ProductInterface $product,
OptionInterface $option,
?OptionInterface $existingOption = null
):void {
$linksToAdd = [];
$linksToUpdate = [];
$linksToDelete = [];

if (is_array($option->getProductLinks())) {
$productLinks = $option->getProductLinks();
foreach ($productLinks as $productLink) {
Expand All @@ -201,20 +217,24 @@ private function updateOptionSelection(ProductInterface $product, OptionInterfac
$linksToUpdate[] = $productLink;
}
}
/** @var LinkInterface[] $linksToDelete */
$linksToDelete = $this->compareLinks($existingLinks, $linksToUpdate);
$linksToUpdate = $this->verifyLinksToUpdate($existingLinks, $linksToUpdate);
if (!empty($existingOption) && !empty($existingOption->getProductLinks())) {
$linksToDelete = $this->compareLinks($existingOption->getProductLinks(), $linksToUpdate);
$linksToUpdate = $this->verifyLinksToUpdate($existingOption->getProductLinks(), $linksToUpdate);
}
}

foreach ($linksToUpdate as $linkedProduct) {
$this->linkManagement->saveChild($product->getSku(), $linkedProduct);
}

foreach ($linksToDelete as $linkedProduct) {
$this->linkManagement->removeChild(
$product->getSku(),
$option->getOptionId(),
$linkedProduct->getSku()
);
}

$this->addChildren->addChildren($product, (int)$option->getOptionId(), $linksToAdd);
}

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

return $result;
}

/**
* Retrieve option from list.
*
* @param Collection $optionCollection
* @param OptionInterface $option
* @param array $existingBundleProductOptions
* @return OptionInterface
*/
private function retrieveExistingOption(
Collection $optionCollection,
OptionInterface $option,
array $existingBundleProductOptions
): OptionInterface {
$existingOption = $optionCollection->getItemById($option->getOptionId());

$incomingOption = current(
array_filter($existingBundleProductOptions, function ($obj) use ($option) {
return $obj->getData()['option_id'] == $option->getId();
})
);

if (!empty($incomingOption)) {
$existingOption->setData(
array_merge(
$existingOption->getData(),
$incomingOption->getData()
)
);
}

// @phpstan-ignore-next-line
if (empty($existingOption)) {
$existingOption = $optionCollection->getNewEmptyItem();
}

return $existingOption;
}
}
26 changes: 13 additions & 13 deletions app/code/Magento/Bundle/Model/Product/SaveHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

/**
* Bundle product save handler
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class SaveHandler implements ExtensionInterface
{
Expand Down Expand Up @@ -107,8 +108,7 @@ public function execute($entity, $arguments = [])

if (!$entity->getCopyFromView()) {
$this->processRemovedOptions($entity, $existingOptionsIds, $optionIds);
$newOptionsIds = array_diff($optionIds, $existingOptionsIds);
$this->saveOptions($entity, $bundleProductOptions, $newOptionsIds);
$this->saveOptions($entity, $bundleProductOptions, $existingBundleProductOptions);
} else {
//save only labels and not selections + product links
$this->saveOptions($entity, $bundleProductOptions);
Expand Down Expand Up @@ -150,20 +150,20 @@ protected function removeOptionLinks($entitySku, $option)
/**
* Perform save for all options entities.
*
* @param object $entity
* @param ProductInterface $entity
* @param array $options
* @param array $newOptionsIds
*
* @param array $existingBundleProductOptions
* @return void
* @throws InputException
* @throws NoSuchEntityException
* @throws \Magento\Framework\Exception\CouldNotSaveException
*/
private function saveOptions($entity, array $options, array $newOptionsIds = []): void
{
foreach ($options as $option) {
if (in_array($option->getOptionId(), $newOptionsIds)) {
$option->setOptionId(null);
}
}
$this->optionSave->saveBulk($entity, $options);
private function saveOptions(
ProductInterface $entity,
array $options,
array $existingBundleProductOptions = []
): void {
$this->optionSave->saveBulk($entity, $options, $existingBundleProductOptions);
}

/**
Expand Down
14 changes: 12 additions & 2 deletions app/code/Magento/Bundle/Pricing/Price/DiscountCalculator.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,21 @@
namespace Magento\Bundle\Pricing\Price;

use Magento\Catalog\Model\Product;
use Magento\Framework\Pricing\PriceCurrencyInterface;

/**
* Class DiscountCalculator
* Check the product available discount and apply the correct discount to the price
*/
class DiscountCalculator
{

/**
* @param PriceCurrencyInterface $priceCurrency
*/
public function __construct(private readonly PriceCurrencyInterface $priceCurrency)
{
}

/**
* Apply percentage discount
*
Expand All @@ -32,6 +41,7 @@ public function calculateDiscount(Product $product, $value = null)
$discount = min($price->getDiscountPercent(), $discount ?: $price->getDiscountPercent());
}
}
return (null !== $discount) ? $discount/100 * $value : $value;
return (null !== $discount) ?
$this->priceCurrency->roundPrice($discount/100 * $value, 2) : $value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,6 @@ public function testSaveBulk()
->method('getMetadata')
->willReturn($metadata);

$this->linkManagement->expects($this->once())
->method('getChildren')
->willReturn([]);
$this->product->expects($this->once())
->method('setIsRelationsChanged')
->with(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
use Magento\Bundle\Pricing\Price\DiscountProviderInterface;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Pricing\Price\FinalPrice;
use Magento\Framework\Pricing\PriceCurrencyInterface;
use Magento\Framework\Pricing\PriceInfo\Base;
use Magento\Framework\Pricing\Price\PriceInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

Expand Down Expand Up @@ -42,21 +44,31 @@ class DiscountCalculatorTest extends TestCase
*/
protected $priceMock;

/**
* @var PriceCurrencyInterface|MockObject
*/
private $priceCurrencyMock;

/**
* Test setUp
*/
protected function setUp(): void
{
$this->productMock = $this->createMock(Product::class);
$this->priceInfoMock = $this->createPartialMock(
Base::class,
['getPrice', 'getPrices']
);
$this->priceInfoMock = $this->getMockBuilder(Base::class)
->disableOriginalConstructor()
->onlyMethods(['getPrice', 'getPrices'])
->addMethods(['getValue'])
->getMock();
$this->finalPriceMock = $this->createMock(FinalPrice::class);
$this->priceMock = $this->getMockForAbstractClass(
DiscountProviderInterface::class
);
$this->calculator = new DiscountCalculator();
$this->priceCurrencyMock = $this->getMockBuilder(PriceCurrencyInterface::class)
->disableOriginalConstructor()
->addMethods(['roundPrice'])
->getMockForAbstractClass();
$this->calculator = new DiscountCalculator($this->priceCurrencyMock);
}

/**
Expand Down Expand Up @@ -98,26 +110,52 @@ public function testCalculateDiscountWithDefaultAmount()
$this->getPriceMock(40),
]
);
$this->priceCurrencyMock->expects($this->once())
->method('roundPrice')
->willReturn(20);
$this->assertEquals(20, $this->calculator->calculateDiscount($this->productMock));
}

/**
* test method calculateDiscount with custom price amount
*
* @dataProvider providerForWithDifferentAmount
*/
public function testCalculateDiscountWithCustomAmount()
public function testCalculateDiscountWithCustomAmount(mixed $discount, mixed $value, float $expectedResult)
{
$this->productMock->expects($this->once())
$this->productMock->expects($this->any())
->method('getPriceInfo')
->willReturn($this->priceInfoMock);
$this->priceInfoMock->expects($this->once())
$this->priceInfoMock->expects($this->any())
->method('getPrices')
->willReturn(
[
$this->getPriceMock(30),
$this->getPriceMock(20),
$this->getPriceMock(40),
]
->willReturn([$this->getPriceMock($discount)]);
if ($value === null) {
$abstractPriceMock = $this->getMockForAbstractClass(
PriceInterface::class
);
$this->assertEquals(10, $this->calculator->calculateDiscount($this->productMock, 50));
$this->priceInfoMock->expects($this->any())
->method('getPrice')
->willReturn($abstractPriceMock);
$abstractPriceMock->expects($this->any())
->method('getValue')
->willReturn($expectedResult);
}
$this->priceCurrencyMock->expects($this->any())
->method('roundPrice')
->willReturn($expectedResult);
$this->assertEquals($expectedResult, $this->calculator->calculateDiscount($this->productMock, $value));
}

/**
* @return array
*/
public function providerForWithDifferentAmount()
{
return [
'test case 1 with discount amount' => [20, 50, 10],
'test case 2 for null discount amount' => [null, 30, 30],
'test case 3 with discount amount' => [99, 5.5, 5.45],
'test case 4 with null value' => [50, null, 50]
];
}
}
Loading

0 comments on commit afa06bd

Please sign in to comment.