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); + } }