Skip to content

Commit a959241

Browse files
committed
Merge remote-tracking branch 'origin/MAGETWO-90144' into 2.3-develop-pr17
2 parents dec9f82 + b7f79fb commit a959241

File tree

6 files changed

+430
-18
lines changed

6 files changed

+430
-18
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\SalesRule\Model;
9+
10+
use Magento\Framework\Pricing\PriceCurrencyInterface;
11+
12+
/**
13+
* Round price and save rounding operation delta.
14+
*/
15+
class DeltaPriceRound
16+
{
17+
/**
18+
* @var PriceCurrencyInterface
19+
*/
20+
private $priceCurrency;
21+
22+
/**
23+
* @var float[]
24+
*/
25+
private $roundingDeltas;
26+
27+
/**
28+
* @param PriceCurrencyInterface $priceCurrency
29+
*/
30+
public function __construct(PriceCurrencyInterface $priceCurrency)
31+
{
32+
$this->priceCurrency = $priceCurrency;
33+
}
34+
35+
/**
36+
* Round price based on previous rounding operation delta.
37+
*
38+
* @param float $price
39+
* @param string $type
40+
* @return float
41+
*/
42+
public function round(float $price, string $type): float
43+
{
44+
if ($price) {
45+
// initialize the delta to a small number to avoid non-deterministic behavior with rounding of 0.5
46+
$delta = isset($this->roundingDeltas[$type]) ? $this->roundingDeltas[$type] : 0.000001;
47+
$price += $delta;
48+
$roundPrice = $this->priceCurrency->round($price);
49+
$this->roundingDeltas[$type] = $price - $roundPrice;
50+
$price = $roundPrice;
51+
}
52+
53+
return $price;
54+
}
55+
56+
/**
57+
* Reset all deltas.
58+
*
59+
* @return void
60+
*/
61+
public function resetAll(): void
62+
{
63+
$this->roundingDeltas = [];
64+
}
65+
66+
/**
67+
* Reset deltas by type.
68+
*
69+
* @param string $type
70+
* @return void
71+
*/
72+
public function reset(string $type): void
73+
{
74+
if (isset($this->roundingDeltas[$type])) {
75+
unset($this->roundingDeltas[$type]);
76+
}
77+
}
78+
}

app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@
55
*/
66
namespace Magento\SalesRule\Model\Rule\Action\Discount;
77

8+
use Magento\Framework\App\ObjectManager;
9+
use Magento\Framework\Pricing\PriceCurrencyInterface;
10+
use Magento\SalesRule\Model\DeltaPriceRound;
11+
use Magento\SalesRule\Model\Validator;
12+
13+
/**
14+
* Calculates discount for cart item if fixed discount applied on whole cart.
15+
*/
816
class CartFixed extends AbstractDiscount
917
{
1018
/**
@@ -14,6 +22,33 @@ class CartFixed extends AbstractDiscount
1422
*/
1523
protected $_cartFixedRuleUsedForAddress = [];
1624

25+
/**
26+
* @var DeltaPriceRound
27+
*/
28+
private $deltaPriceRound;
29+
30+
/**
31+
* @var string
32+
*/
33+
private static $discountType = 'CartFixed';
34+
35+
/**
36+
* @param Validator $validator
37+
* @param DataFactory $discountDataFactory
38+
* @param PriceCurrencyInterface $priceCurrency
39+
* @param DeltaPriceRound $deltaPriceRound
40+
*/
41+
public function __construct(
42+
Validator $validator,
43+
DataFactory $discountDataFactory,
44+
PriceCurrencyInterface $priceCurrency,
45+
DeltaPriceRound $deltaPriceRound
46+
) {
47+
$this->deltaPriceRound = $deltaPriceRound;
48+
49+
parent::__construct($validator, $discountDataFactory, $priceCurrency);
50+
}
51+
1752
/**
1853
* @param \Magento\SalesRule\Model\Rule $rule
1954
* @param \Magento\Quote\Model\Quote\Item\AbstractItem $item
@@ -51,14 +86,22 @@ public function calculate($rule, $item, $qty)
5186
$cartRules[$rule->getId()] = $rule->getDiscountAmount();
5287
}
5388

54-
if ($cartRules[$rule->getId()] > 0) {
89+
$availableDiscountAmount = (float)$cartRules[$rule->getId()];
90+
$discountType = self::$discountType . $rule->getId();
91+
92+
if ($availableDiscountAmount > 0) {
5593
$store = $quote->getStore();
5694
if ($ruleTotals['items_count'] <= 1) {
57-
$quoteAmount = $this->priceCurrency->convert($cartRules[$rule->getId()], $store);
58-
$baseDiscountAmount = min($baseItemPrice * $qty, $cartRules[$rule->getId()]);
95+
$quoteAmount = $this->priceCurrency->convert($availableDiscountAmount, $store);
96+
$baseDiscountAmount = min($baseItemPrice * $qty, $availableDiscountAmount);
97+
$this->deltaPriceRound->reset($discountType);
5998
} else {
60-
$discountRate = $baseItemPrice * $qty / $ruleTotals['base_items_price'];
61-
$maximumItemDiscount = $rule->getDiscountAmount() * $discountRate;
99+
$ratio = $baseItemPrice * $qty / $ruleTotals['base_items_price'];
100+
$maximumItemDiscount = $this->deltaPriceRound->round(
101+
$rule->getDiscountAmount() * $ratio,
102+
$discountType
103+
);
104+
62105
$quoteAmount = $this->priceCurrency->convert($maximumItemDiscount, $store);
63106

64107
$baseDiscountAmount = min($baseItemPrice * $qty, $maximumItemDiscount);
@@ -67,7 +110,11 @@ public function calculate($rule, $item, $qty)
67110

68111
$baseDiscountAmount = $this->priceCurrency->round($baseDiscountAmount);
69112

70-
$cartRules[$rule->getId()] -= $baseDiscountAmount;
113+
$availableDiscountAmount -= $baseDiscountAmount;
114+
$cartRules[$rule->getId()] = $availableDiscountAmount;
115+
if ($availableDiscountAmount <= 0) {
116+
$this->deltaPriceRound->reset($discountType);
117+
}
71118

72119
$discountData->setAmount($this->priceCurrency->round(min($itemPrice * $qty, $quoteAmount)));
73120
$discountData->setBaseAmount($baseDiscountAmount);
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\SalesRule\Test\Unit\Model;
9+
10+
use Magento\Framework\Pricing\PriceCurrencyInterface;
11+
use Magento\SalesRule\Model\DeltaPriceRound;
12+
13+
/**
14+
* Tests for Magento\SalesRule\Model\DeltaPriceRound.
15+
*/
16+
class DeltaPriceRoundTest extends \PHPUnit\Framework\TestCase
17+
{
18+
/**
19+
* @var PriceCurrencyInterface|\PHPUnit_Framework_MockObject_MockObject
20+
*/
21+
private $priceCurrency;
22+
23+
/**
24+
* @var DeltaPriceRound
25+
*/
26+
private $model;
27+
28+
/**
29+
* @inheritdoc
30+
*/
31+
protected function setUp()
32+
{
33+
$this->priceCurrency = $this->getMockForAbstractClass(PriceCurrencyInterface::class);
34+
$this->priceCurrency->method('round')
35+
->willReturnCallback(
36+
function ($amount) {
37+
return round($amount, 2);
38+
}
39+
);
40+
41+
$this->model = new DeltaPriceRound($this->priceCurrency);
42+
}
43+
44+
/**
45+
* Tests rounded price based on previous rounding operation delta.
46+
*
47+
* @param array $prices
48+
* @param array $roundedPrices
49+
* @return void
50+
* @dataProvider roundDataProvider
51+
*/
52+
public function testRound(array $prices, array $roundedPrices): void
53+
{
54+
foreach ($prices as $key => $price) {
55+
$roundedPrice = $this->model->round($price, 'test');
56+
$this->assertEquals($roundedPrices[$key], $roundedPrice);
57+
}
58+
59+
$this->model->reset('test');
60+
}
61+
62+
/**
63+
* @return array
64+
*/
65+
public function roundDataProvider(): array
66+
{
67+
return [
68+
[
69+
'prices' => [1.004, 1.004],
70+
'rounded prices' => [1.00, 1.01],
71+
],
72+
[
73+
'prices' => [1.005, 1.005],
74+
'rounded prices' => [1.01, 1.0],
75+
],
76+
];
77+
}
78+
79+
/**
80+
* @return void
81+
*/
82+
public function testReset(): void
83+
{
84+
$this->assertEquals(1.44, $this->model->round(1.444, 'test'));
85+
$this->model->reset('test');
86+
$this->assertEquals(1.44, $this->model->round(1.444, 'test'));
87+
}
88+
89+
/**
90+
* @return void
91+
*/
92+
public function testResetAll(): void
93+
{
94+
$this->assertEquals(1.44, $this->model->round(1.444, 'test1'));
95+
$this->assertEquals(1.44, $this->model->round(1.444, 'test2'));
96+
97+
$this->model->resetAll();
98+
99+
$this->assertEquals(1.44, $this->model->round(1.444, 'test1'));
100+
$this->assertEquals(1.44, $this->model->round(1.444, 'test2'));
101+
}
102+
}

app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/Discount/CartFixedTest.php

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,48 +5,56 @@
55
*/
66
namespace Magento\SalesRule\Test\Unit\Model\Rule\Action\Discount;
77

8+
use PHPUnit_Framework_MockObject_MockObject as MockObject;
9+
10+
/**
11+
* Tests for Magento\SalesRule\Model\Rule\Action\Discount\CartFixed.
12+
*/
813
class CartFixedTest extends \PHPUnit\Framework\TestCase
914
{
1015
/**
11-
* @var \Magento\SalesRule\Model\Rule|\PHPUnit_Framework_MockObject_MockObject
16+
* @var \Magento\SalesRule\Model\Rule|MockObject
1217
*/
1318
protected $rule;
1419

1520
/**
16-
* @var \Magento\Quote\Model\Quote\Item\AbstractItem|\PHPUnit_Framework_MockObject_MockObject
21+
* @var \Magento\Quote\Model\Quote\Item\AbstractItem|MockObject
1722
*/
1823
protected $item;
1924

2025
/**
21-
* @var \Magento\SalesRule\Model\Validator|\PHPUnit_Framework_MockObject_MockObject
26+
* @var \Magento\SalesRule\Model\Validator|MockObject
2227
*/
2328
protected $validator;
2429

2530
/**
26-
* @var \Magento\SalesRule\Model\Rule\Action\Discount\Data|\PHPUnit_Framework_MockObject_MockObject
31+
* @var \Magento\SalesRule\Model\Rule\Action\Discount\Data|MockObject
2732
*/
2833
protected $data;
2934

3035
/**
31-
* @var \Magento\Quote\Model\Quote|\PHPUnit_Framework_MockObject_MockObject
36+
* @var \Magento\Quote\Model\Quote|MockObject
3237
*/
3338
protected $quote;
3439

3540
/**
36-
* @var \Magento\Quote\Model\Quote\Address|\PHPUnit_Framework_MockObject_MockObject
41+
* @var \Magento\Quote\Model\Quote\Address|MockObject
3742
*/
3843
protected $address;
3944

4045
/**
41-
* @var CartFixed
46+
* @var \Magento\SalesRule\Model\Rule\Action\Discount\CartFixed
4247
*/
4348
protected $model;
4449

4550
/**
46-
* @var \Magento\Framework\Pricing\PriceCurrencyInterface|\PHPUnit_Framework_MockObject_MockObject
51+
* @var \Magento\Framework\Pricing\PriceCurrencyInterface|MockObject
4752
*/
4853
protected $priceCurrency;
4954

55+
/**
56+
* @inheritdoc
57+
*/
5058
protected function setUp()
5159
{
5260
$this->rule = $this->getMockBuilder(\Magento\Framework\DataObject::class)
@@ -66,18 +74,23 @@ protected function setUp()
6674
$this->item->expects($this->any())->method('getAddress')->will($this->returnValue($this->address));
6775

6876
$this->validator = $this->createMock(\Magento\SalesRule\Model\Validator::class);
77+
/** @var \Magento\SalesRule\Model\Rule\Action\Discount\DataFactory|MockObject $dataFactory */
6978
$dataFactory = $this->createPartialMock(
7079
\Magento\SalesRule\Model\Rule\Action\Discount\DataFactory::class,
7180
['create']
7281
);
7382
$dataFactory->expects($this->any())->method('create')->will($this->returnValue($this->data));
74-
$this->priceCurrency = $this->getMockBuilder(
75-
\Magento\Framework\Pricing\PriceCurrencyInterface::class
76-
)->getMock();
83+
$this->priceCurrency = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class)
84+
->getMock();
85+
$deltaPriceRound = $this->getMockBuilder(\Magento\SalesRule\Model\DeltaPriceRound::class)
86+
->disableOriginalConstructor()
87+
->getMock();
88+
7789
$this->model = new \Magento\SalesRule\Model\Rule\Action\Discount\CartFixed(
7890
$this->validator,
7991
$dataFactory,
80-
$this->priceCurrency
92+
$this->priceCurrency,
93+
$deltaPriceRound
8194
);
8295
}
8396

0 commit comments

Comments
 (0)