diff --git a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php
index 2ae1c1c7ac63a..e44200614fa00 100644
--- a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php
+++ b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php
@@ -64,25 +64,13 @@ public function calculate($rule, $item, $qty)
$ruleTotals = $this->validator->getRuleItemTotalsInfo($rule->getId());
$quote = $item->getQuote();
- $address = $item->getAddress();
$itemPrice = $this->validator->getItemPrice($item);
$baseItemPrice = $this->validator->getItemBasePrice($item);
$itemOriginalPrice = $this->validator->getItemOriginalPrice($item);
$baseItemOriginalPrice = $this->validator->getItemBaseOriginalPrice($item);
- /**
- * prevent applying whole cart discount for every shipping order, but only for first order
- */
- if ($quote->getIsMultiShipping()) {
- $usedForAddressId = $this->getCartFixedRuleUsedForAddress($rule->getId());
- if ($usedForAddressId && $usedForAddressId != $address->getId()) {
- return $discountData;
- } else {
- $this->setCartFixedRuleUsedForAddress($rule->getId(), $address->getId());
- }
- }
- $cartRules = $address->getCartFixedRules();
+ $cartRules = $quote->getCartFixedRules();
if (!isset($cartRules[$rule->getId()])) {
$cartRules[$rule->getId()] = $rule->getDiscountAmount();
}
@@ -122,7 +110,7 @@ public function calculate($rule, $item, $qty)
$discountData->setOriginalAmount(min($itemOriginalPrice * $qty, $quoteAmount));
$discountData->setBaseOriginalAmount($this->priceCurrency->round($baseItemOriginalPrice));
}
- $address->setCartFixedRules($cartRules);
+ $quote->setCartFixedRules($cartRules);
return $discountData;
}
@@ -130,6 +118,7 @@ public function calculate($rule, $item, $qty)
/**
* Set information about usage cart fixed rule by quote address
*
+ * @deprecated should be removed as it is not longer used
* @param int $ruleId
* @param int $itemId
* @return void
@@ -142,6 +131,7 @@ protected function setCartFixedRuleUsedForAddress($ruleId, $itemId)
/**
* Retrieve information about usage cart fixed rule by quote address
*
+ * @deprecated should be removed as it is not longer used
* @param int $ruleId
* @return int|null
*/
diff --git a/app/code/Magento/SalesRule/Model/Rule/QuoteResetAppliedRules.php b/app/code/Magento/SalesRule/Model/Rule/QuoteResetAppliedRules.php
new file mode 100644
index 0000000000000..5c1d98b65a0e4
--- /dev/null
+++ b/app/code/Magento/SalesRule/Model/Rule/QuoteResetAppliedRules.php
@@ -0,0 +1,24 @@
+setCartFixedRules([]);
+ }
+}
diff --git a/app/code/Magento/SalesRule/Model/Spi/QuoteResetAppliedRulesInterface.php b/app/code/Magento/SalesRule/Model/Spi/QuoteResetAppliedRulesInterface.php
new file mode 100644
index 0000000000000..ba73ab83608db
--- /dev/null
+++ b/app/code/Magento/SalesRule/Model/Spi/QuoteResetAppliedRulesInterface.php
@@ -0,0 +1,22 @@
+resetAppliedRules = $resetAppliedRules;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(Observer $observer)
+ {
+ $this->resetAppliedRules->execute($observer->getQuote());
+ }
+}
diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/Discount/CartFixedTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/Discount/CartFixedTest.php
index 13f26124c464d..cafe194d05de0 100644
--- a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/Discount/CartFixedTest.php
+++ b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/Discount/CartFixedTest.php
@@ -65,10 +65,17 @@ protected function setUp()
$this->item = $this->createMock(\Magento\Quote\Model\Quote\Item\AbstractItem::class);
$this->data = $this->createPartialMock(\Magento\SalesRule\Model\Rule\Action\Discount\Data::class, []);
- $this->quote = $this->createMock(\Magento\Quote\Model\Quote::class);
+ $this->quote = $this->createPartialMock(
+ \Magento\Quote\Model\Quote::class,
+ [
+ 'getStore',
+ 'getCartFixedRules',
+ 'setCartFixedRules',
+ ]
+ );
$this->address = $this->createPartialMock(
\Magento\Quote\Model\Quote\Address::class,
- ['getCartFixedRules', 'setCartFixedRules', '__wakeup']
+ ['__wakeup']
);
$this->item->expects($this->any())->method('getQuote')->will($this->returnValue($this->quote));
$this->item->expects($this->any())->method('getAddress')->will($this->returnValue($this->address));
@@ -101,7 +108,7 @@ public function testCalculate()
{
$this->rule->setData(['id' => 1, 'discount_amount' => 10.0]);
- $this->address->expects($this->any())->method('getCartFixedRules')->will($this->returnValue([]));
+ $this->quote->expects($this->any())->method('getCartFixedRules')->will($this->returnValue([]));
$store = $this->createMock(\Magento\Store\Model\Store::class);
$this->priceCurrency->expects($this->atLeastOnce())->method('convert')->will($this->returnArgument(0));
$this->priceCurrency->expects($this->atLeastOnce())->method('round')->will($this->returnArgument(0));
@@ -145,7 +152,7 @@ public function testCalculate()
$this->returnValue(100)
);
- $this->address->expects($this->once())->method('setCartFixedRules')->with([1 => 0.0]);
+ $this->quote->expects($this->once())->method('setCartFixedRules')->with([1 => 0.0]);
$this->model->calculate($this->rule, $this->item, 1);
$this->assertEquals($this->data->getAmount(), 10);
diff --git a/app/code/Magento/SalesRule/etc/di.xml b/app/code/Magento/SalesRule/etc/di.xml
index 0e5fe6d29aed6..c4bc9c3a6decb 100644
--- a/app/code/Magento/SalesRule/etc/di.xml
+++ b/app/code/Magento/SalesRule/etc/di.xml
@@ -36,6 +36,8 @@
type="Magento\SalesRule\Model\Data\DiscountData" />
+
diff --git a/app/code/Magento/SalesRule/etc/events.xml b/app/code/Magento/SalesRule/etc/events.xml
index 5f899fb0cca5c..c55c37de71aac 100644
--- a/app/code/Magento/SalesRule/etc/events.xml
+++ b/app/code/Magento/SalesRule/etc/events.xml
@@ -36,4 +36,7 @@
+
+
+
diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Action/Discount/CartFixedTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Action/Discount/CartFixedTest.php
index f56fb0a2c6135..583316d97d68c 100644
--- a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Action/Discount/CartFixedTest.php
+++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Action/Discount/CartFixedTest.php
@@ -10,7 +10,9 @@
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\ProductRepository;
+use Magento\Checkout\Model\Session as CheckoutSession;
use Magento\Framework\Api\SearchCriteriaBuilder;
+use Magento\Multishipping\Model\Checkout\Type\Multishipping;
use Magento\Quote\Api\CartRepositoryInterface;
use Magento\Quote\Api\Data\CartItemInterface;
use Magento\Quote\Api\GuestCartItemRepositoryInterface;
@@ -21,6 +23,10 @@
use Magento\Quote\Model\QuoteIdMask;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
+use Magento\Sales\Model\Order;
+use Magento\Sales\Model\Order\Email\Sender\OrderSender;
+use Magento\SalesRule\Api\RuleRepositoryInterface;
+use Magento\SalesRule\Model\Rule;
use Magento\TestFramework\Helper\Bootstrap;
/**
@@ -187,11 +193,12 @@ public function testDiscountsOnQuoteWithFixedDiscount(): void
/**
* Load cart from fixture.
*
+ * @param string $reservedOrderId
* @return Quote
*/
- private function getQuote(): Quote
+ private function getQuote(string $reservedOrderId = 'test01'): Quote
{
- $searchCriteria = $this->criteriaBuilder->addFilter('reserved_order_id', 'test01')->create();
+ $searchCriteria = $this->criteriaBuilder->addFilter('reserved_order_id', $reservedOrderId)->create();
$carts = $this->quoteRepository->getList($searchCriteria)
->getItems();
if (!$carts) {
@@ -290,4 +297,283 @@ private function getOrder(string $incrementId): OrderInterface
return array_pop($items);
}
+
+ /**
+ * Checks "fixed amount discount for whole cart" with multiple orders with different shipping addresses
+ *
+ * @magentoAppIsolation enabled
+ * @magentoDataFixture Magento/SalesRule/_files/coupon_cart_fixed_discount.php
+ * @magentoDataFixture Magento/Multishipping/Fixtures/quote_with_split_items.php
+ * @dataProvider multishippingDataProvider
+ * @param float $discount
+ * @param array $firstOrderTotals
+ * @param array $secondOrderTotals
+ * @param array $thirdOrderTotals
+ * @return void
+ */
+ public function testMultishipping(
+ float $discount,
+ array $firstOrderTotals,
+ array $secondOrderTotals,
+ array $thirdOrderTotals
+ ): void {
+ $store = $this->objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore();
+ $salesRule = $this->getRule('15$ fixed discount on whole cart');
+ $salesRule->setDiscountAmount($discount);
+ $this->saveRule($salesRule);
+ $quote = $this->getQuote('multishipping_quote_id');
+ $quote->setStoreId($store->getId());
+ $quote->setCouponCode('CART_FIXED_DISCOUNT_15');
+ $quote->collectTotals();
+ $this->quoteRepository->save($quote);
+ /** @var CheckoutSession $session */
+ $session = $this->objectManager->get(CheckoutSession::class);
+ $session->replaceQuote($quote);
+ $orderSender = $this->getMockBuilder(OrderSender::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $model = $this->objectManager->create(
+ Multishipping::class,
+ ['orderSender' => $orderSender]
+ );
+ $model->createOrders();
+ $orderList = $this->getOrderList((int)$quote->getId());
+ $this->assertCount(3, $orderList);
+ /**
+ * The order with $10 simple product
+ * @var Order $firstOrder
+ */
+ $firstOrder = array_shift($orderList);
+
+ $this->assertEquals(
+ $firstOrderTotals['subtotal'],
+ $firstOrder->getSubtotal()
+ );
+ $this->assertEquals(
+ $firstOrderTotals['discount_amount'],
+ $firstOrder->getDiscountAmount()
+ );
+ $this->assertEquals(
+ $firstOrderTotals['shipping_amount'],
+ $firstOrder->getShippingAmount()
+ );
+ $this->assertEquals(
+ $firstOrderTotals['grand_total'],
+ $firstOrder->getGrandTotal()
+ );
+ /**
+ * The order with $20 simple product
+ * @var Order $secondOrder
+ */
+ $secondOrder = array_shift($orderList);
+ $this->assertEquals(
+ $secondOrderTotals['subtotal'],
+ $secondOrder->getSubtotal()
+ );
+ $this->assertEquals(
+ $secondOrderTotals['discount_amount'],
+ $secondOrder->getDiscountAmount()
+ );
+ $this->assertEquals(
+ $secondOrderTotals['shipping_amount'],
+ $secondOrder->getShippingAmount()
+ );
+ $this->assertEquals(
+ $secondOrderTotals['grand_total'],
+ $secondOrder->getGrandTotal()
+ );
+ /**
+ * The order with $5 virtual product and billing address as shipping
+ * @var Order $thirdOrder
+ */
+ $thirdOrder = array_shift($orderList);
+ $this->assertEquals(
+ $thirdOrderTotals['subtotal'],
+ $thirdOrder->getSubtotal()
+ );
+ $this->assertEquals(
+ $thirdOrderTotals['discount_amount'],
+ $thirdOrder->getDiscountAmount()
+ );
+ $this->assertEquals(
+ $thirdOrderTotals['shipping_amount'],
+ $thirdOrder->getShippingAmount()
+ );
+ $this->assertEquals(
+ $thirdOrderTotals['grand_total'],
+ $thirdOrder->getGrandTotal()
+ );
+ }
+
+ /**
+ * @return array
+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
+ */
+ public function multishippingDataProvider(): array
+ {
+ return [
+ 'Discount < 1stOrderSubtotal: only 1st order gets discount' => [
+ 5,
+ [
+ 'subtotal' => 10.00,
+ 'discount_amount' => -5.00,
+ 'shipping_amount' => 5.00,
+ 'grand_total' => 10.00,
+ ],
+ [
+ 'subtotal' => 20.00,
+ 'discount_amount' => -0.00,
+ 'shipping_amount' => 5.00,
+ 'grand_total' => 25.00,
+ ],
+ [
+ 'subtotal' => 5.00,
+ 'discount_amount' => -0.00,
+ 'shipping_amount' => 0.00,
+ 'grand_total' => 5.00,
+ ]
+ ],
+ 'Discount = 1stOrderSubtotal: only 1st order gets discount' => [
+ 10,
+ [
+ 'subtotal' => 10.00,
+ 'discount_amount' => -10.00,
+ 'shipping_amount' => 5.00,
+ 'grand_total' => 5.00,
+ ],
+ [
+ 'subtotal' => 20.00,
+ 'discount_amount' => -0.00,
+ 'shipping_amount' => 5.00,
+ 'grand_total' => 25.00,
+ ],
+ [
+ 'subtotal' => 5.00,
+ 'discount_amount' => -0.00,
+ 'shipping_amount' => 0.00,
+ 'grand_total' => 5.00,
+ ]
+ ],
+ 'Discount > 1stOrderSubtotal: 1st order get 100% discount and 2nd order get the remaining discount' => [
+ 15,
+ [
+ 'subtotal' => 10.00,
+ 'discount_amount' => -10.00,
+ 'shipping_amount' => 5.00,
+ 'grand_total' => 5.00,
+ ],
+ [
+ 'subtotal' => 20.00,
+ 'discount_amount' => -5.00,
+ 'shipping_amount' => 5.00,
+ 'grand_total' => 20.00,
+ ],
+ [
+ 'subtotal' => 5.00,
+ 'discount_amount' => -0.00,
+ 'shipping_amount' => 0.00,
+ 'grand_total' => 5.00,
+ ]
+ ],
+ 'Discount = 1stOrderSubtotal + 2ndOrderSubtotal: 1st order and 2nd order get 100% discount' => [
+ 30,
+ [
+ 'subtotal' => 10.00,
+ 'discount_amount' => -10.00,
+ 'shipping_amount' => 5.00,
+ 'grand_total' => 5.00,
+ ],
+ [
+ 'subtotal' => 20.00,
+ 'discount_amount' => -20.00,
+ 'shipping_amount' => 5.00,
+ 'grand_total' => 5.00,
+ ],
+ [
+ 'subtotal' => 5.00,
+ 'discount_amount' => -0.00,
+ 'shipping_amount' => 0.00,
+ 'grand_total' => 5.00,
+ ]
+ ],
+ 'Discount > 1stOrdSubtotal + 2ndOrdSubtotal: 1st order and 2nd order get 100% discount
+ and 3rd order get remaining discount' => [
+ 31,
+ [
+ 'subtotal' => 10.00,
+ 'discount_amount' => -10.00,
+ 'shipping_amount' => 5.00,
+ 'grand_total' => 5.00,
+ ],
+ [
+ 'subtotal' => 20.00,
+ 'discount_amount' => -20.00,
+ 'shipping_amount' => 5.00,
+ 'grand_total' => 5.00,
+ ],
+ [
+ 'subtotal' => 5.00,
+ 'discount_amount' => -1.00,
+ 'shipping_amount' => 0.00,
+ 'grand_total' => 4.00,
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * Get list of orders by quote id.
+ *
+ * @param int $quoteId
+ * @return array
+ */
+ private function getOrderList(int $quoteId): array
+ {
+ /** @var SearchCriteriaBuilder $searchCriteriaBuilder */
+ $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class);
+ $searchCriteria = $searchCriteriaBuilder->addFilter('quote_id', $quoteId)
+ ->create();
+
+ /** @var OrderRepositoryInterface $orderRepository */
+ $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class);
+ return $orderRepository->getList($searchCriteria)->getItems();
+ }
+
+ /**
+ * Get rule by name
+ *
+ * @param string $name
+ * @return Rule
+ */
+ private function getRule(string $name): Rule
+ {
+ /** @var SearchCriteriaBuilder $searchCriteriaBuilder */
+ $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class);
+ $searchCriteria = $searchCriteriaBuilder->addFilter('name', $name)
+ ->create();
+ /** @var RuleRepositoryInterface $ruleRepository */
+ $ruleRepository = $this->objectManager->get(RuleRepositoryInterface::class);
+ $items = $ruleRepository->getList($searchCriteria)
+ ->getItems();
+ /** @var Rule $salesRule */
+ $dataModel = array_pop($items);
+ /** @var Rule $ruleModel */
+ $ruleModel = $this->objectManager->get(\Magento\SalesRule\Model\RuleFactory::class)->create();
+ $ruleModel->load($dataModel->getRuleId());
+ return $ruleModel;
+ }
+
+ /**
+ * Save rule into database
+ *
+ * @param Rule $rule
+ * @return void
+ */
+ private function saveRule(Rule $rule): void
+ {
+ /** @var \Magento\SalesRule\Model\ResourceModel\Rule $resourceModel */
+ $resourceModel = $this->objectManager->get(\Magento\SalesRule\Model\ResourceModel\Rule::class);
+ $resourceModel->save($rule);
+ }
}