diff --git a/app/code/Magento/SalesRule/Helper/CartFixedDiscount.php b/app/code/Magento/SalesRule/Helper/CartFixedDiscount.php index b1fda3e78672b..025f2c7215964 100644 --- a/app/code/Magento/SalesRule/Helper/CartFixedDiscount.php +++ b/app/code/Magento/SalesRule/Helper/CartFixedDiscount.php @@ -89,6 +89,31 @@ public function getDiscountAmount( ); } + /** + * Get discount amount for item calculated proportionally based on already applied discount + * + * @param float $ruleDiscount + * @param float $qty + * @param float $baseItemPrice + * @param float $baseItemDiscountAmount + * @param float $baseRuleTotalsDiscount + * @param string $discountType + * @return float + */ + public function getDiscountedAmountProportionally( + float $ruleDiscount, + float $qty, + float $baseItemPrice, + float $baseItemDiscountAmount, + float $baseRuleTotalsDiscount, + string $discountType + ): float { + $baseItemPriceTotal = $baseItemPrice * $qty - $baseItemDiscountAmount; + $ratio = $baseItemPriceTotal / $baseRuleTotalsDiscount; + $discountAmount = $this->deltaPriceRound->round($ruleDiscount * $ratio, $discountType); + return $discountAmount; + } + /** * Get shipping discount amount * @@ -186,10 +211,6 @@ public function getBaseRuleTotals( $baseRuleTotals = ($quote->getIsMultiShipping() && $isMultiShipping) ? $this->getQuoteTotalsForMultiShipping($quote) : $this->getQuoteTotalsForRegularShipping($address, $baseRuleTotals); - } else { - if ($quote->getIsMultiShipping() && $isMultiShipping) { - $baseRuleTotals = $quote->getBaseSubtotal(); - } } return (float) $baseRuleTotals; } diff --git a/app/code/Magento/SalesRule/Model/Quote/Discount.php b/app/code/Magento/SalesRule/Model/Quote/Discount.php index a32fe249920b1..0663e57a0a0f8 100644 --- a/app/code/Magento/SalesRule/Model/Quote/Discount.php +++ b/app/code/Magento/SalesRule/Model/Quote/Discount.php @@ -11,6 +11,7 @@ use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Api\Data\ShippingAssignmentInterface; use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; use Magento\Quote\Model\Quote\Address\Total; use Magento\Quote\Model\Quote\Address\Total\AbstractTotal; use Magento\Quote\Model\Quote\Item; @@ -20,8 +21,10 @@ use Magento\SalesRule\Api\Data\RuleDiscountInterfaceFactory; use Magento\SalesRule\Model\Data\RuleDiscount; use Magento\SalesRule\Model\Discount\PostProcessorFactory; +use Magento\SalesRule\Model\Rule; use Magento\SalesRule\Model\Validator; use Magento\Store\Model\StoreManagerInterface; +use Magento\SalesRule\Model\RulesApplier; /** * Discount totals calculation model. @@ -66,6 +69,16 @@ class Discount extends AbstractTotal */ private $discountDataInterfaceFactory; + /** + * @var RulesApplier|null + */ + private $rulesApplier; + + /** + * @var array + */ + private $addressDiscountAggregator = []; + /** * @param ManagerInterface $eventManager * @param StoreManagerInterface $storeManager @@ -73,6 +86,7 @@ class Discount extends AbstractTotal * @param PriceCurrencyInterface $priceCurrency * @param RuleDiscountInterfaceFactory|null $discountInterfaceFactory * @param DiscountDataInterfaceFactory|null $discountDataInterfaceFactory + * @param RulesApplier|null $rulesApplier */ public function __construct( ManagerInterface $eventManager, @@ -80,7 +94,8 @@ public function __construct( Validator $validator, PriceCurrencyInterface $priceCurrency, RuleDiscountInterfaceFactory $discountInterfaceFactory = null, - DiscountDataInterfaceFactory $discountDataInterfaceFactory = null + DiscountDataInterfaceFactory $discountDataInterfaceFactory = null, + RulesApplier $rulesApplier = null ) { $this->setCode(self::COLLECTOR_TYPE_CODE); $this->eventManager = $eventManager; @@ -91,6 +106,8 @@ public function __construct( ?: ObjectManager::getInstance()->get(RuleDiscountInterfaceFactory::class); $this->discountDataInterfaceFactory = $discountDataInterfaceFactory ?: ObjectManager::getInstance()->get(DiscountDataInterfaceFactory::class); + $this->rulesApplier = $rulesApplier + ?: ObjectManager::getInstance()->get(RulesApplier::class); } /** @@ -102,6 +119,7 @@ public function __construct( * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function collect( Quote $quote, @@ -109,81 +127,98 @@ public function collect( Total $total ) { parent::collect($quote, $shippingAssignment, $total); - $store = $this->storeManager->getStore($quote->getStoreId()); + /** @var Address $address */ $address = $shippingAssignment->getShipping()->getAddress(); - if ($quote->currentPaymentWasSet()) { $address->setPaymentMethod($quote->getPayment()->getMethod()); } - $this->calculator->reset($address); - - $items = $shippingAssignment->getItems(); - if (!count($items)) { + $itemsAggregate = []; + foreach ($shippingAssignment->getItems() as $item) { + $itemId = $item->getId(); + $itemsAggregate[$itemId] = $item; + } + $items = []; + foreach ($quote->getAllAddresses() as $quoteAddress) { + foreach ($quoteAddress->getAllItems() as $item) { + $items[] = $item; + } + } + if (!$items || !$itemsAggregate) { return $this; } - $eventArgs = [ 'website_id' => $store->getWebsiteId(), 'customer_group_id' => $quote->getCustomerGroupId(), 'coupon_code' => $quote->getCouponCode(), ]; - - $this->calculator->init($store->getWebsiteId(), $quote->getCustomerGroupId(), $quote->getCouponCode()); - $this->calculator->initTotals($items, $address); - $address->setDiscountDescription([]); - $items = $this->calculator->sortItemsByPriority($items, $address); $address->getExtensionAttributes()->setDiscounts([]); - $addressDiscountAggregator = []; - - /** @var Item $item */ + $this->addressDiscountAggregator = []; + $address->setCartFixedRules([]); + $quote->setCartFixedRules([]); foreach ($items as $item) { - if ($item->getNoDiscount() || !$this->calculator->canApplyDiscount($item)) { - $item->setDiscountAmount(0); - $item->setBaseDiscountAmount(0); - - // ensure my children are zeroed out - if ($item->getHasChildren() && $item->isChildrenCalculated()) { - foreach ($item->getChildren() as $child) { - $child->setDiscountAmount(0); - $child->setBaseDiscountAmount(0); - } + $this->rulesApplier->setAppliedRuleIds($item, []); + if ($item->getExtensionAttributes()) { + $item->getExtensionAttributes()->setDiscounts(null); + } + $item->setDiscountAmount(0); + $item->setBaseDiscountAmount(0); + $item->setDiscountPercent(0); + if ($item->getChildren() && $item->isChildrenCalculated()) { + foreach ($item->getChildren() as $child) { + $child->setDiscountAmount(0); + $child->setBaseDiscountAmount(0); + $child->setDiscountPercent(0); + } + } + } + $this->calculator->init($store->getWebsiteId(), $quote->getCustomerGroupId(), $quote->getCouponCode()); + $this->calculator->initTotals($items, $address); + $items = $this->calculator->sortItemsByPriority($items, $address); + $rules = $this->calculator->getRules($address); + /** @var Rule $rule */ + foreach ($rules as $rule) { + /** @var Item $item */ + foreach ($items as $item) { + if ($item->getNoDiscount() || !$this->calculator->canApplyDiscount($item) || $item->getParentItem()) { + continue; } + $eventArgs['item'] = $item; + $this->eventManager->dispatch('sales_quote_address_discount_item', $eventArgs); + $this->calculator->process($item, $rule); + } + $appliedRuleIds = $quote->getAppliedRuleIds() ? explode(',', $quote->getAppliedRuleIds()) : []; + if ($rule->getStopRulesProcessing() && in_array($rule->getId(), $appliedRuleIds)) { + break; + } + $this->calculator->initTotals($items, $address); + } + foreach ($items as $item) { + if (!isset($itemsAggregate[$item->getId()])) { continue; } - // to determine the child item discount, we calculate the parent if ($item->getParentItem()) { continue; - } - - $eventArgs['item'] = $item; - $this->eventManager->dispatch('sales_quote_address_discount_item', $eventArgs); - - if ($item->getHasChildren() && $item->isChildrenCalculated()) { - $this->calculator->process($item); + } elseif ($item->getHasChildren() && $item->isChildrenCalculated()) { foreach ($item->getChildren() as $child) { $eventArgs['item'] = $child; $this->eventManager->dispatch('sales_quote_address_discount_item', $eventArgs); $this->aggregateItemDiscount($child, $total); } - } else { - $this->calculator->process($item); - $this->aggregateItemDiscount($item, $total); } + $this->aggregateItemDiscount($item, $total); if ($item->getExtensionAttributes()) { - $this->aggregateDiscountPerRule($item, $address, $addressDiscountAggregator); + $this->aggregateDiscountPerRule($item, $address); } } - $this->calculator->prepareDescription($address); $total->setDiscountDescription($address->getDiscountDescription()); $total->setSubtotalWithDiscount($total->getSubtotal() + $total->getDiscountAmount()); $total->setBaseSubtotalWithDiscount($total->getBaseSubtotal() + $total->getBaseDiscountAmount()); $address->setDiscountAmount($total->getDiscountAmount()); $address->setBaseDiscountAmount($total->getBaseDiscountAmount()); - return $this; } @@ -273,13 +308,11 @@ public function fetch(Quote $quote, Total $total) * * @param AbstractItem $item * @param AddressInterface $address - * @param array $addressDiscountAggregator * @return void */ private function aggregateDiscountPerRule( AbstractItem $item, - AddressInterface $address, - array &$addressDiscountAggregator + AddressInterface $address ) { $discountBreakdown = $item->getExtensionAttributes()->getDiscounts(); if ($discountBreakdown) { @@ -288,15 +321,17 @@ private function aggregateDiscountPerRule( $discount = $value->getDiscountData(); $ruleLabel = $value->getRuleLabel(); $ruleID = $value->getRuleID(); - if (isset($addressDiscountAggregator[$ruleID])) { + if (isset($this->addressDiscountAggregator[$ruleID])) { /** @var RuleDiscount $cartDiscount */ - $cartDiscount = $addressDiscountAggregator[$ruleID]; + $cartDiscount = $this->addressDiscountAggregator[$ruleID]; $discountData = $cartDiscount->getDiscountData(); - $discountData->setBaseAmount($discountData->getBaseAmount()+$discount->getBaseAmount()); - $discountData->setAmount($discountData->getAmount()+$discount->getAmount()); - $discountData->setOriginalAmount($discountData->getOriginalAmount()+$discount->getOriginalAmount()); + $discountData->setBaseAmount($discountData->getBaseAmount() + $discount->getBaseAmount()); + $discountData->setAmount($discountData->getAmount() + $discount->getAmount()); + $discountData->setOriginalAmount( + $discountData->getOriginalAmount() + $discount->getOriginalAmount() + ); $discountData->setBaseOriginalAmount( - $discountData->getBaseOriginalAmount()+$discount->getBaseOriginalAmount() + $discountData->getBaseOriginalAmount() + $discount->getBaseOriginalAmount() ); } else { $data = [ @@ -313,10 +348,10 @@ private function aggregateDiscountPerRule( ]; /** @var RuleDiscount $cartDiscount */ $cartDiscount = $this->discountInterfaceFactory->create(['data' => $data]); - $addressDiscountAggregator[$ruleID] = $cartDiscount; + $this->addressDiscountAggregator[$ruleID] = $cartDiscount; } } } - $address->getExtensionAttributes()->setDiscounts(array_values($addressDiscountAggregator)); + $address->getExtensionAttributes()->setDiscounts(array_values($this->addressDiscountAggregator)); } } 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 0adeedc32f759..9794dc1628dae 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php +++ b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php @@ -79,11 +79,13 @@ public function calculate($rule, $item, $qty) $ruleTotals = $this->validator->getRuleItemTotalsInfo($rule->getId()); $baseRuleTotals = $ruleTotals['base_items_price'] ?? 0.0; + $baseRuleTotalsDiscount = $ruleTotals['base_items_discount_amount'] ?? 0.0; + $ruleItemsCount = $ruleTotals['items_count'] ?? 0; $address = $item->getAddress(); + $quote = $item->getQuote(); $shippingMethod = $address->getShippingMethod(); $isAppliedToShipping = (int) $rule->getApplyToShipping(); - $quote = $item->getQuote(); $ruleDiscount = (float) $rule->getDiscountAmount(); $isMultiShipping = $this->cartFixedDiscountHelper->checkMultiShippingQuote($quote); @@ -91,6 +93,7 @@ public function calculate($rule, $item, $qty) $baseItemPrice = $this->validator->getItemBasePrice($item); $itemOriginalPrice = $this->validator->getItemOriginalPrice($item); $baseItemOriginalPrice = $this->validator->getItemBaseOriginalPrice($item); + $baseItemDiscountAmount = (float) $item->getBaseDiscountAmount(); $cartRules = $quote->getCartFixedRules(); if (!isset($cartRules[$rule->getId()])) { @@ -101,17 +104,17 @@ public function calculate($rule, $item, $qty) if ($availableDiscountAmount > 0) { $store = $quote->getStore(); - if ($ruleTotals['items_count'] <= 1) { - $baseRuleTotals = $shippingMethod ? - $this->cartFixedDiscountHelper - ->getBaseRuleTotals( - $isAppliedToShipping, - $quote, - $isMultiShipping, - $address, - $baseRuleTotals - ) : $baseRuleTotals; - $maximumItemDiscount = $this->cartFixedDiscountHelper + $baseRuleTotals = $shippingMethod ? + $this->cartFixedDiscountHelper + ->getBaseRuleTotals( + $isAppliedToShipping, + $quote, + $isMultiShipping, + $address, + $baseRuleTotals + ) : $baseRuleTotals; + if ($isAppliedToShipping) { + $baseDiscountAmount = $this->cartFixedDiscountHelper ->getDiscountAmount( $ruleDiscount, $qty, @@ -119,29 +122,22 @@ public function calculate($rule, $item, $qty) $baseRuleTotals, $discountType ); - $quoteAmount = $this->priceCurrency->convert($maximumItemDiscount, $store); - $baseDiscountAmount = min($baseItemPrice * $qty, $maximumItemDiscount); - $this->deltaPriceRound->reset($discountType); } else { - $baseRuleTotals = $shippingMethod ? - $this->cartFixedDiscountHelper - ->getBaseRuleTotals( - $isAppliedToShipping, - $quote, - $isMultiShipping, - $address, - $baseRuleTotals - ) : $baseRuleTotals; - $maximumItemDiscount =$this->cartFixedDiscountHelper - ->getDiscountAmount( + $baseDiscountAmount = $this->cartFixedDiscountHelper + ->getDiscountedAmountProportionally( $ruleDiscount, $qty, $baseItemPrice, - $baseRuleTotals, + $baseItemDiscountAmount, + $baseRuleTotals - $baseRuleTotalsDiscount, $discountType ); - $quoteAmount = $this->priceCurrency->convert($maximumItemDiscount, $store); - $baseDiscountAmount = min($baseItemPrice * $qty, $maximumItemDiscount); + } + $discountAmount = $this->priceCurrency->convert($baseDiscountAmount, $store); + $baseDiscountAmount = min($baseItemPrice * $qty, $baseDiscountAmount); + if ($ruleItemsCount <= 1) { + $this->deltaPriceRound->reset($discountType); + } else { $this->validator->decrementRuleItemTotalsCount($rule->getId()); } @@ -162,24 +158,24 @@ public function calculate($rule, $item, $qty) $ruleTotals['items_count'] <= 1) { $estimatedShippingAmount = (float) $address->getBaseShippingInclTax(); $shippingDiscountAmount = $this->cartFixedDiscountHelper-> - getShippingDiscountAmount( - $rule, - $estimatedShippingAmount, - $baseRuleTotals - ); + getShippingDiscountAmount( + $rule, + $estimatedShippingAmount, + $baseRuleTotals + ); $cartRules[$rule->getId()] -= $shippingDiscountAmount; if ($cartRules[$rule->getId()] < 0.0) { $baseDiscountAmount += $cartRules[$rule->getId()]; - $quoteAmount += $cartRules[$rule->getId()]; + $discountAmount += $cartRules[$rule->getId()]; } } if ($availableDiscountAmount <= 0) { $this->deltaPriceRound->reset($discountType); } - $discountData->setAmount($this->priceCurrency->roundPrice(min($itemPrice * $qty, $quoteAmount))); + $discountData->setAmount($this->priceCurrency->roundPrice(min($itemPrice * $qty, $discountAmount))); $discountData->setBaseAmount($baseDiscountAmount); - $discountData->setOriginalAmount(min($itemOriginalPrice * $qty, $quoteAmount)); + $discountData->setOriginalAmount(min($itemOriginalPrice * $qty, $discountAmount)); $discountData->setBaseOriginalAmount($this->priceCurrency->roundPrice($baseItemOriginalPrice)); } $quote->setCartFixedRules($cartRules); diff --git a/app/code/Magento/SalesRule/Model/RulesApplier.php b/app/code/Magento/SalesRule/Model/RulesApplier.php index cde8e5c065502..3652f22798c14 100644 --- a/app/code/Magento/SalesRule/Model/RulesApplier.php +++ b/app/code/Magento/SalesRule/Model/RulesApplier.php @@ -10,7 +10,6 @@ use Magento\SalesRule\Model\Data\RuleDiscount; use Magento\SalesRule\Model\Quote\ChildrenValidationLocator; use Magento\Framework\App\ObjectManager; -use Magento\SalesRule\Model\ResourceModel\Rule\Collection; use Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory; use Magento\SalesRule\Model\Rule\Action\Discount\Data; use Magento\SalesRule\Model\Rule\Action\Discount\DataFactory; @@ -99,7 +98,7 @@ public function __construct( * Apply rules to current order item * * @param AbstractItem $item - * @param Collection $rules + * @param array $rules * @param bool $skipValidation * @param mixed $couponCode * @return array @@ -109,10 +108,6 @@ public function applyRules($item, $rules, $skipValidation, $couponCode) { $address = $item->getAddress(); $appliedRuleIds = []; - $this->discountAggregator = []; - if ($item->getExtensionAttributes()) { - $item->getExtensionAttributes()->setDiscounts(null); - } /* @var $rule Rule */ foreach ($rules as $rule) { if (!$this->validatorUtility->canProcessRule($rule, $address)) { @@ -138,10 +133,6 @@ public function applyRules($item, $rules, $skipValidation, $couponCode) $this->applyRule($item, $rule, $address, $couponCode); $appliedRuleIds[$rule->getRuleId()] = $rule->getRuleId(); - - if ($rule->getStopRulesProcessing()) { - break; - } } return $appliedRuleIds; @@ -270,10 +261,9 @@ private function setDiscountBreakdown($discountData, $item, $rule, $address) ]; /** @var RuleDiscount $itemDiscount */ $ruleDiscount = $this->discountInterfaceFactory->create(['data' => $data]); - $this->discountAggregator[] = $ruleDiscount; - $item->getExtensionAttributes()->setDiscounts($this->discountAggregator); + $this->discountAggregator[$item->getId()][$rule->getId()] = $ruleDiscount; + $item->getExtensionAttributes()->setDiscounts(array_values($this->discountAggregator[$item->getId()])); $parentItem = $item->getParentItem(); - if ($parentItem && $parentItem->getExtensionAttributes()) { $this->aggregateDiscountBreakdown($discountData, $parentItem, $rule, $address); } @@ -281,6 +271,14 @@ private function setDiscountBreakdown($discountData, $item, $rule, $address) return $this; } + /** + * Reset discount aggregator + */ + public function resetDiscountAggregator() + { + $this->discountAggregator = []; + } + /** * Add Discount Breakdown to existing discount data * @@ -413,7 +411,7 @@ public function setAppliedRuleIds(AbstractItem $item, array $appliedRuleIds) $address = $item->getAddress(); $quote = $item->getQuote(); - $item->setAppliedRuleIds(join(',', $appliedRuleIds)); + $item->setAppliedRuleIds($this->validatorUtility->mergeIds($item->getAppliedRuleIds(), $appliedRuleIds)); $address->setAppliedRuleIds($this->validatorUtility->mergeIds($address->getAppliedRuleIds(), $appliedRuleIds)); $quote->setAppliedRuleIds($this->validatorUtility->mergeIds($quote->getAppliedRuleIds(), $appliedRuleIds)); diff --git a/app/code/Magento/SalesRule/Model/Validator.php b/app/code/Magento/SalesRule/Model/Validator.php index 7b3a6b15b7a32..26fb7ee721732 100644 --- a/app/code/Magento/SalesRule/Model/Validator.php +++ b/app/code/Magento/SalesRule/Model/Validator.php @@ -12,6 +12,7 @@ use Magento\Quote\Model\Quote\Item\AbstractItem; use Magento\SalesRule\Helper\CartFixedDiscount; use Magento\SalesRule\Model\ResourceModel\Rule\CollectionFactory; +use Magento\SalesRule\Model\ResourceModel\Rule\Collection as RulesCollection; /** * SalesRule Validator Model @@ -31,7 +32,7 @@ class Validator extends \Magento\Framework\Model\AbstractModel /** * Rule source collection * - * @var \Magento\SalesRule\Model\ResourceModel\Rule\Collection + * @var RulesCollection */ protected $_rules; @@ -58,7 +59,7 @@ class Validator extends \Magento\Framework\Model\AbstractModel protected $_skipActionsValidation = false; /** - * Catalog data + * Catalog data helper * * @var \Magento\Catalog\Helper\Data|null */ @@ -169,11 +170,24 @@ public function init($websiteId, $customerGroupId, $couponCode) /** * Get rules collection for current object state * + * @deprecated use getRules * @param Address|null $address - * @return \Magento\SalesRule\Model\ResourceModel\Rule\Collection + * @return RulesCollection * @throws \Zend_Db_Select_Exception */ protected function _getRules(Address $address = null) + { + return $this->getRules($address); + } + + /** + * Get rules collection for current object state + * + * @param Address|null $address + * @return RulesCollection + * @throws \Zend_Db_Select_Exception + */ + public function getRules(Address $address = null) { $addressId = $this->getAddressId($address); $key = $this->getWebsiteId() . '_' @@ -240,7 +254,7 @@ public function setSkipActionsValidation($flag) public function canApplyRules(AbstractItem $item) { $address = $item->getAddress(); - foreach ($this->_getRules($address) as $rule) { + foreach ($this->getRules($address) as $rule) { if (!$this->validatorUtility->canProcessRule($rule, $address) || !$rule->getActions()->validate($item)) { return false; } @@ -260,6 +274,7 @@ public function reset(Address $address) $this->validatorUtility->resetRoundingDeltas(); $address->setBaseSubtotalWithDiscount($address->getBaseSubtotal()); $address->setSubtotalWithDiscount($address->getSubtotal()); + $this->rulesApplier->resetDiscountAggregator(); if ($this->_isFirstTimeResetRun) { $address->setAppliedRuleIds(''); $address->getQuote()->setAppliedRuleIds(''); @@ -273,22 +288,12 @@ public function reset(Address $address) * Quote item discount calculation process * * @param AbstractItem $item + * @param Rule $rule * @return $this * @throws \Zend_Db_Select_Exception */ - public function process(AbstractItem $item) + public function process(AbstractItem $item, Rule $rule) { - $item->setDiscountAmount(0); - $item->setBaseDiscountAmount(0); - $item->setDiscountPercent(0); - if ($item->getChildren() && $item->isChildrenCalculated()) { - foreach ($item->getChildren() as $child) { - $child->setDiscountAmount(0); - $child->setBaseDiscountAmount(0); - $child->setDiscountPercent(0); - } - } - $itemPrice = $this->getItemPrice($item); if ($itemPrice < 0) { return $this; @@ -296,7 +301,7 @@ public function process(AbstractItem $item) $appliedRuleIds = $this->rulesApplier->applyRules( $item, - $this->_getRules($item->getAddress()), + [$rule], $this->_skipActionsValidation, $this->getCouponCode() ); @@ -326,7 +331,7 @@ public function processShippingAmount(Address $address) } $quote = $address->getQuote(); $appliedRuleIds = []; - foreach ($this->_getRules($address) as $rule) { + foreach ($this->getRules($address) as $rule) { /* @var Rule $rule */ if (!$rule->getApplyToShipping() || !$this->validatorUtility->canProcessRule($rule, $address)) { continue; @@ -429,43 +434,51 @@ public function processShippingAmount(Address $address) * @param mixed $items * @param Address $address * @return $this + * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @throws \Zend_Validate_Exception * @throws \Zend_Db_Select_Exception */ public function initTotals($items, Address $address) { - $address->setCartFixedRules([]); - if (!$items) { return $this; } /** @var Rule $rule */ - foreach ($this->_getRules($address) as $rule) { - if (Rule::CART_FIXED_ACTION == $rule->getSimpleAction() - && $this->validatorUtility->canProcessRule($rule, $address) + foreach ($this->getRules($address) as $rule) { + if (Rule::CART_FIXED_ACTION !== $rule->getSimpleAction() + || !$this->validatorUtility->canProcessRule($rule, $address) ) { - $ruleTotalItemsPrice = 0; - $ruleTotalBaseItemsPrice = 0; - $validItemsCount = 0; - - foreach ($items as $item) { - //Skipping child items to avoid double calculations - if (!$this->isValidItemForRule($item, $rule)) { - continue; - } - $qty = $this->validatorUtility->getItemQty($item, $rule); - $ruleTotalItemsPrice += $this->getItemPrice($item) * $qty; - $ruleTotalBaseItemsPrice += $this->getItemBasePrice($item) * $qty; - $validItemsCount++; + continue; + } + $ruleTotalItemsPrice = 0; + $ruleTotalBaseItemsPrice = 0; + $ruleTotalItemsDiscountAmount = 0; + $ruleTotalBaseItemsDiscountAmount = 0; + $validItemsCount = 0; + + foreach ($items as $item) { + if (!$this->isValidItemForRule($item, $rule) + || ($item->getChildren() && $item->isChildrenCalculated()) + || $item->getNoDiscount() + ) { + continue; } - - $this->_rulesItemTotals[$rule->getId()] = [ - 'items_price' => $ruleTotalItemsPrice, - 'base_items_price' => $ruleTotalBaseItemsPrice, - 'items_count' => $validItemsCount, - ]; + $qty = $this->validatorUtility->getItemQty($item, $rule); + $ruleTotalItemsPrice += $this->getItemPrice($item) * $qty; + $ruleTotalBaseItemsPrice += $this->getItemBasePrice($item) * $qty; + $ruleTotalItemsDiscountAmount += $item->getDiscountAmount(); + $ruleTotalBaseItemsDiscountAmount += $item->getBaseDiscountAmount(); + $validItemsCount++; } + + $this->_rulesItemTotals[$rule->getId()] = [ + 'items_price' => $ruleTotalItemsPrice, + 'items_discount_amount' => $ruleTotalItemsDiscountAmount, + 'base_items_price' => $ruleTotalBaseItemsPrice, + 'base_items_discount_amount' => $ruleTotalBaseItemsDiscountAmount, + 'items_count' => $validItemsCount, + ]; } return $this; @@ -480,12 +493,6 @@ public function initTotals($items, Address $address) */ private function isValidItemForRule(AbstractItem $item, Rule $rule) { - if ($item->getParentItemId()) { - return false; - } - if ($item->getParentItem()) { - return false; - } if (!$rule->getActions()->validate($item)) { return false; } @@ -613,7 +620,7 @@ public function sortItemsByPriority($items, Address $address = null) { $itemsSorted = []; /** @var $rule Rule */ - foreach ($this->_getRules($address) as $rule) { + foreach ($this->getRules($address) as $rule) { foreach ($items as $itemKey => $itemValue) { if ($rule->getActions()->validate($itemValue)) { unset($items[$itemKey]); diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php index 5633bd788de86..5a7d6142a6d43 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php @@ -9,20 +9,24 @@ use Magento\Framework\Api\ExtensionAttributesInterface; use Magento\Framework\Event\Manager; +use Magento\Framework\Event\ManagerInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Quote\Api\Data\ShippingAssignmentInterface; use Magento\Quote\Api\Data\ShippingInterface; use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address; use Magento\Quote\Model\Quote\Address\Total; use Magento\Quote\Model\Quote\Item; +use Magento\SalesRule\Api\Data\DiscountDataInterfaceFactory; +use Magento\SalesRule\Api\Data\RuleDiscountInterfaceFactory; use Magento\SalesRule\Model\Quote\Discount; use Magento\SalesRule\Model\Rule\Action\Discount\Data; use Magento\SalesRule\Model\Rule\Action\Discount\DataFactory; +use Magento\SalesRule\Model\RulesApplier; use Magento\SalesRule\Model\Validator; use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManager; +use Magento\SalesRule\Model\Rule; +use Magento\Store\Model\StoreManagerInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -36,11 +40,6 @@ class DiscountTest extends TestCase */ protected $discount; - /** - * @var ObjectManager - */ - protected $objectManager; - /** * @var MockObject */ @@ -71,13 +70,36 @@ class DiscountTest extends TestCase */ private $discountFactory; + /** + * @var Rule|MockObject + */ + private $rule; + + /** + * @var RuleDiscountInterfaceFactory|MockObject + */ + private $discountInterfaceFactoryMock; + + /** + * @var DiscountDataInterfaceFactory|MockObject + */ + private $discountDataInterfaceFactoryMock; + + /** + * @var RulesApplier|MockObject + */ + private $rulesApplierMock; + protected function setUp(): void { - $this->objectManager = new ObjectManager($this); - $this->storeManagerMock = $this->createMock(StoreManager::class); + $this->storeManagerMock = $this->getMockForAbstractClass(StoreManagerInterface::class); + $this->eventManagerMock = $this->getMockForAbstractClass(ManagerInterface::class); + $this->discountInterfaceFactoryMock = $this->createMock(RuleDiscountInterfaceFactory::class); + $this->discountDataInterfaceFactoryMock = $this->createMock(DiscountDataInterfaceFactory::class); + $this->rulesApplierMock = $this->createMock(RulesApplier::class); $this->validatorMock = $this->getMockBuilder(Validator::class) ->disableOriginalConstructor() - ->setMethods( + ->onlyMethods( [ 'canApplyRules', 'reset', @@ -88,6 +110,16 @@ protected function setUp(): void 'process', 'processShippingAmount', 'canApplyDiscount', + 'getRules', + 'prepareDescription' + ] + ) + ->getMock(); + $this->rule = $this->getMockBuilder(Rule::class) + ->disableOriginalConstructor() + ->addMethods( + [ + 'getSimpleAction' ] ) ->getMock(); @@ -108,7 +140,7 @@ function ($argument) { ->getMock(); $addressExtension = $this->getMockBuilder( ExtensionAttributesInterface::class - )->setMethods(['setDiscounts', 'getDiscounts'])->getMock(); + )->addMethods(['setDiscounts', 'getDiscounts'])->getMockForAbstractClass(); $addressExtension->method('getDiscounts')->willReturn([]); $addressExtension->expects($this->any()) ->method('setDiscounts') @@ -130,14 +162,14 @@ function ($argument) { ); /** @var Discount $discount */ - $this->discount = $this->objectManager->getObject( - Discount::class, - [ - 'storeManager' => $this->storeManagerMock, - 'validator' => $this->validatorMock, - 'eventManager' => $this->eventManagerMock, - 'priceCurrency' => $priceCurrencyMock, - ] + $this->discount = new Discount( + $this->eventManagerMock, + $this->storeManagerMock, + $this->validatorMock, + $priceCurrencyMock, + $this->discountInterfaceFactoryMock, + $this->discountDataInterfaceFactoryMock, + $this->rulesApplierMock ); $discountData = $this->getMockBuilder(Data::class) ->getMock(); @@ -151,29 +183,38 @@ public function testCollectItemNoDiscount() { $itemNoDiscount = $this->getMockBuilder(Item::class) ->addMethods(['getNoDiscount']) - ->onlyMethods(['getExtensionAttributes']) + ->onlyMethods(['getExtensionAttributes', 'getParentItem', 'getId']) ->disableOriginalConstructor() ->getMock(); $itemExtension = $this->getMockBuilder( ExtensionAttributesInterface::class - )->setMethods(['setDiscounts', 'getDiscounts'])->getMock(); + )->addMethods(['setDiscounts', 'getDiscounts'])->getMockForAbstractClass(); $itemExtension->method('getDiscounts')->willReturn([]); $itemExtension->expects($this->any()) ->method('setDiscounts') ->willReturn([]); - $itemNoDiscount->expects( - $this->any() - )->method('getExtensionAttributes')->willReturn($itemExtension); + $itemNoDiscount->expects($this->any())->method('getExtensionAttributes')->willReturn($itemExtension); + $itemNoDiscount->expects($this->any())->method('getId')->willReturn(1); $itemNoDiscount->expects($this->once())->method('getNoDiscount')->willReturn(true); $this->validatorMock->expects($this->once())->method('sortItemsByPriority') ->with([$itemNoDiscount], $this->addressMock) ->willReturnArgument(0); + $this->validatorMock->expects($this->once())->method('getRules') + ->with($this->addressMock) + ->willReturn([$this->rule]); + $this->rule->expects($this->any())->method('getSimpleAction') + ->willReturn(null); $storeMock = $this->getMockBuilder(Store::class) ->addMethods(['getStore']) ->disableOriginalConstructor() ->getMock(); $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($storeMock); - $quoteMock = $this->createMock(Quote::class); + $quoteMock = $this->getMockBuilder(Quote::class) + ->onlyMethods(['getAllAddresses', 'getStoreId']) + ->disableOriginalConstructor() + ->getMock(); + $quoteMock->expects($this->any())->method('getAllAddresses')->willReturn([$this->addressMock]); + $this->addressMock->expects($this->any())->method('getAllItems')->willReturn([$itemNoDiscount]); $this->addressMock->expects($this->any())->method('getQuote')->willReturn($quoteMock); $this->shippingAssignmentMock->expects($this->any())->method('getItems')->willReturn([$itemNoDiscount]); $this->addressMock->expects($this->any())->method('getShippingAmount')->willReturn(true); @@ -190,16 +231,23 @@ public function testCollectItemHasParent() { $itemWithParentId = $this->getMockBuilder(Item::class) ->addMethods(['getNoDiscount']) - ->onlyMethods(['getParentItem']) + ->onlyMethods(['getParentItem', 'getId', 'getExtensionAttributes']) ->disableOriginalConstructor() ->getMock(); $itemWithParentId->expects($this->once())->method('getNoDiscount')->willReturn(false); - $itemWithParentId->expects($this->once())->method('getParentItem')->willReturn(true); + $itemWithParentId->expects($this->any())->method('getId')->willReturn(1); + $itemWithParentId->expects($this->any())->method('getParentItem')->willReturn(true); + $itemWithParentId->expects($this->any())->method('getExtensionAttributes')->willReturn(false); $this->validatorMock->expects($this->any())->method('canApplyDiscount')->willReturn(true); $this->validatorMock->expects($this->any())->method('sortItemsByPriority') ->with([$itemWithParentId], $this->addressMock) ->willReturnArgument(0); + $this->validatorMock->expects($this->once())->method('getRules') + ->with($this->addressMock) + ->willReturn([$this->rule]); + $this->rule->expects($this->any())->method('getSimpleAction') + ->willReturn(null); $storeMock = $this->getMockBuilder(Store::class) ->addMethods(['getStore']) @@ -207,7 +255,12 @@ public function testCollectItemHasParent() ->getMock(); $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($storeMock); - $quoteMock = $this->createMock(Quote::class); + $quoteMock = $this->getMockBuilder(Quote::class) + ->onlyMethods(['getAllAddresses', 'getStoreId']) + ->disableOriginalConstructor() + ->getMock(); + $quoteMock->expects($this->any())->method('getAllAddresses')->willReturn([$this->addressMock]); + $this->addressMock->expects($this->any())->method('getAllItems')->willReturn([$itemWithParentId]); $this->addressMock->expects($this->any())->method('getQuote')->willReturn($quoteMock); $this->addressMock->expects($this->any())->method('getShippingAmount')->willReturn(true); @@ -224,35 +277,46 @@ public function testCollectItemHasNoChildren() { $itemWithChildren = $this->getMockBuilder(Item::class) ->disableOriginalConstructor() - ->setMethods( + ->onlyMethods( [ - 'getNoDiscount', 'getParentItem', - 'getHasChildren', 'isChildrenCalculated', 'getChildren', 'getExtensionAttributes', + 'getId', + ] + )->addMethods( + [ + 'getNoDiscount', + 'getHasChildren', ] ) ->getMock(); $itemExtension = $this->getMockBuilder( ExtensionAttributesInterface::class - )->setMethods(['setDiscounts', 'getDiscounts'])->getMock(); + )->addMethods(['setDiscounts', 'getDiscounts', 'getId'])->getMock(); $itemExtension->method('getDiscounts')->willReturn([]); $itemExtension->expects($this->any()) ->method('setDiscounts') ->willReturn([]); + $itemExtension->expects($this->any())->method('getId')->willReturn(1); $itemWithChildren->expects( $this->any() )->method('getExtensionAttributes')->willReturn($itemExtension); $itemWithChildren->expects($this->once())->method('getNoDiscount')->willReturn(false); - $itemWithChildren->expects($this->once())->method('getParentItem')->willReturn(false); + $itemWithChildren->expects($this->any())->method('getParentItem')->willReturn(false); $itemWithChildren->expects($this->once())->method('getHasChildren')->willReturn(false); + $itemWithChildren->expects($this->any())->method('getId')->willReturn(2); $this->validatorMock->expects($this->any())->method('canApplyDiscount')->willReturn(true); $this->validatorMock->expects($this->once())->method('sortItemsByPriority') ->with([$itemWithChildren], $this->addressMock) ->willReturnArgument(0); + $this->validatorMock->expects($this->once())->method('getRules') + ->with($this->addressMock) + ->willReturn([$this->rule]); + $this->rule->expects($this->any())->method('getSimpleAction') + ->willReturn(null); $storeMock = $this->getMockBuilder(Store::class) ->disableOriginalConstructor() @@ -261,8 +325,11 @@ public function testCollectItemHasNoChildren() $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($storeMock); $quoteMock = $this->getMockBuilder(Quote::class) + ->onlyMethods(['getAllAddresses', 'getStoreId']) ->disableOriginalConstructor() ->getMock(); + $quoteMock->expects($this->any())->method('getAllAddresses')->willReturn([$this->addressMock]); + $this->addressMock->expects($this->any())->method('getAllItems')->willReturn([$itemWithChildren]); $this->addressMock->expects($this->any())->method('getQuote')->willReturn($quoteMock); $this->addressMock->expects($this->any())->method('getShippingAmount')->willReturn(true); $this->shippingAssignmentMock->expects($this->any())->method('getItems')->willReturn([$itemWithChildren]); 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 276a308980683..1889febcedd38 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 @@ -121,9 +121,10 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMock(); $this->cartFixedDiscountHelper = $this->getMockBuilder(CartFixedDiscount::class) - ->setMethods([ + ->onlyMethods([ 'calculateShippingAmountWhenAppliedToShipping', 'getDiscountAmount', + 'getDiscountedAmountProportionally', 'checkMultiShippingQuote', 'getQuoteTotalsForMultiShipping', 'getQuoteTotalsForRegularShipping', @@ -170,7 +171,7 @@ public function testCalculate(array $shipping, array $ruleDetails): void ); $this->cartFixedDiscountHelper ->expects($this->any()) - ->method('getDiscountAmount') + ->method('getDiscountedAmountProportionally') ->will( $this->returnValue( $ruleDetails['discounted_amount'] diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php index 50824a87316cb..af6f41cee2294 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php @@ -98,7 +98,7 @@ protected function setUp(): void * @return void * @dataProvider dataProviderChildren */ - public function testApplyRulesWhenRuleWithStopRulesProcessingIsUsed( + public function testApplyRules( bool $isChildren, bool $isContinue ): void { @@ -116,30 +116,19 @@ public function testApplyRulesWhenRuleWithStopRulesProcessingIsUsed( ->with($this->anything()) ->willReturn($discountData); /** - * @var Rule|MockObject $ruleWithStopFurtherProcessing + * @var Rule|MockObject $rule */ - $ruleWithStopFurtherProcessing = $this->getMockBuilder(Rule::class) + $rule = $this->getMockBuilder(Rule::class) ->addMethods(['getCouponType', 'getRuleId']) ->onlyMethods(['getStoreLabel', 'getActions']) ->disableOriginalConstructor() ->getMock(); - /** - * @var Rule|MockObject $ruleThatShouldNotBeRun - */ - $ruleThatShouldNotBeRun = $this->getMockBuilder(Rule::class) - ->addMethods(['getStopRulesProcessing']) - ->disableOriginalConstructor() - ->getMock(); $actionMock = $this->getMockBuilder(Collection::class) ->addMethods(['validate']) ->disableOriginalConstructor() ->getMock(); - $ruleWithStopFurtherProcessing->setName('ruleWithStopFurtherProcessing'); - $ruleThatShouldNotBeRun->setName('ruleThatShouldNotBeRun'); - $rules = [$ruleWithStopFurtherProcessing, $ruleThatShouldNotBeRun]; - $item->setDiscountCalculationPrice($positivePrice); $item->setData('calculation_price', $positivePrice); @@ -151,7 +140,7 @@ public function testApplyRulesWhenRuleWithStopRulesProcessingIsUsed( ->method('canProcessRule') ->willReturn(true); - $ruleWithStopFurtherProcessing->expects($this->atLeastOnce()) + $rule->expects($this->atLeastOnce()) ->method('getActions') ->willReturn($actionMock); @@ -174,18 +163,14 @@ public function testApplyRulesWhenRuleWithStopRulesProcessingIsUsed( } if (!$isContinue || !$isChildren) { - $ruleWithStopFurtherProcessing->expects($this->any()) + $rule->expects($this->any()) ->method('getRuleId') ->willReturn($ruleId); - $this->applyRule($item, $ruleWithStopFurtherProcessing); - - $ruleWithStopFurtherProcessing->setStopRulesProcessing(true); - $ruleThatShouldNotBeRun->expects($this->never()) - ->method('getStopRulesProcessing'); + $this->applyRule($item, $rule); } - $result = $this->rulesApplier->applyRules($item, $rules, $skipValidation, $couponCode); + $result = $this->rulesApplier->applyRules($item, [$rule], $skipValidation, $couponCode); $this->assertEquals($appliedRuleIds, $result); } diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php index 993ee04a6aaf4..82ca394effff5 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php @@ -274,6 +274,7 @@ public function testProcess(): void { $negativePrice = -1; + $rule = $this->createMock(Rule::class); $this->item->setDiscountCalculationPrice($negativePrice); $this->item->setData('calculation_price', $negativePrice); @@ -284,34 +285,7 @@ public function testProcess(): void $this->model->getCustomerGroupId(), $this->model->getCouponCode() ); - $this->model->process($this->item); - } - - /** - * @return void - */ - public function testProcessWhenItemPriceIsNegativeDiscountsAreZeroed(): void - { - $negativePrice = -1; - $nonZeroDiscount = 123; - $this->model->init( - $this->model->getWebsiteId(), - $this->model->getCustomerGroupId(), - $this->model->getCouponCode() - ); - - $this->item->setDiscountCalculationPrice($negativePrice); - $this->item->setData('calculation_price', $negativePrice); - - $this->item->setDiscountAmount($nonZeroDiscount); - $this->item->setBaseDiscountAmount($nonZeroDiscount); - $this->item->setDiscountPercent($nonZeroDiscount); - - $this->model->process($this->item); - - $this->assertEquals(0, $this->item->getDiscountAmount()); - $this->assertEquals(0, $this->item->getBaseDiscountAmount()); - $this->assertEquals(0, $this->item->getDiscountPercent()); + $this->model->process($this->item, $rule); } /** @@ -328,6 +302,7 @@ public function testApplyRulesThatAppliedRuleIdsAreCollected(): void $this->model->getCustomerGroupId(), $this->model->getCouponCode() ); + $rule = $this->createMock(Rule::class); $this->item->setDiscountCalculationPrice($positivePrice); $this->item->setData('calculation_price', $positivePrice); @@ -337,7 +312,7 @@ public function testApplyRulesThatAppliedRuleIdsAreCollected(): void ->method('applyRules') ->with( $this->item, - $this->ruleCollection, + [$rule], $this->anything(), $this->anything() ) @@ -349,7 +324,7 @@ public function testApplyRulesThatAppliedRuleIdsAreCollected(): void $expectedRuleIds ); - $this->model->process($this->item); + $this->model->process($this->item, $rule); } /** diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_simple_products.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_simple_products.php new file mode 100644 index 0000000000000..00e7c35c44a80 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_simple_products.php @@ -0,0 +1,32 @@ +requireDataFixture('Magento/Catalog/_files/multiple_products.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->create(ProductRepositoryInterface::class); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var CartInterface $quote */ +$quote = $objectManager->get(CartInterfaceFactory::class)->create(); +$quote->setIsActive(true) + ->setStoreId(1) + ->setCheckoutMethod(Onepage::METHOD_GUEST) + ->setReservedOrderId('test_quote_with_simple_products'); +$quote->addProduct($productRepository->get('simple1'), 1); +$quote->addProduct($productRepository->get('simple2'), 1); +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_simple_products_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_simple_products_rollback.php new file mode 100644 index 0000000000000..b47beadb76ff1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_simple_products_rollback.php @@ -0,0 +1,28 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $order Quote */ +$quoteCollection = Bootstrap::getObjectManager()->create(Collection::class); +foreach ($quoteCollection as $quote) { + $quote->delete(); +} + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products_rollback.php'); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); 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 7ddb38ac94e5d..39b67e5c60214 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 @@ -427,7 +427,7 @@ public function testMultishipping( public function multishippingDataProvider(): array { return [ - 'Discount < 1stOrderSubtotal: only 1st order gets discount' => [ + 'Discount $5 proportionally spread between products' => [ 5, [ 'subtotal' => 10.00, @@ -443,54 +443,12 @@ public function multishippingDataProvider(): array ], [ 'subtotal' => 5.00, - 'discount_amount' => -5.00, - 'shipping_amount' => 0.00, - 'grand_total' => 0.00, - ] - ], - 'Discount = 1stOrderSubtotal: only 1st order gets discount' => [ - 10, - [ - 'subtotal' => 10.00, - 'discount_amount' => -2.8600, - 'shipping_amount' => 5.00, - 'grand_total' => 12.1400, - ], - [ - 'subtotal' => 20.00, - 'discount_amount' => -5.71, - 'shipping_amount' => 5.00, - 'grand_total' => 19.2900, - ], - [ - 'subtotal' => 5.00, - 'discount_amount' => -5.00, + 'discount_amount' => -0.71, 'shipping_amount' => 0.00, - 'grand_total' => 0.00, + 'grand_total' => 4.2900, ] ], - 'Discount > 1stOrderSubtotal: 1st order get 100% discount and 2nd order get the remaining discount' => [ - 15, - [ - 'subtotal' => 10.00, - 'discount_amount' => -4.2900, - 'shipping_amount' => 5.00, - 'grand_total' => 10.71, - ], - [ - 'subtotal' => 20.00, - 'discount_amount' => -8.5700, - 'shipping_amount' => 5.00, - 'grand_total' => 16.43, - ], - [ - 'subtotal' => 5.00, - 'discount_amount' => -5.00, - 'shipping_amount' => 0.00, - 'grand_total' => 0.00, - ] - ], - 'Discount = 1stOrderSubtotal + 2ndOrderSubtotal: 1st order and 2nd order get 100% discount' => [ + 'Discount $30 proportionally spread between products' => [ 30, [ 'subtotal' => 10.00, @@ -506,36 +464,67 @@ public function multishippingDataProvider(): array ], [ 'subtotal' => 5.00, - 'discount_amount' => -5.00, + 'discount_amount' => -4.29, 'shipping_amount' => 0.00, - 'grand_total' => 0.00, + 'grand_total' => 0.7100, ] ], - 'Discount > 1stOrdSubtotal + 2ndOrdSubtotal: 1st order and 2nd order get 100% discount' - . ' and 3rd order get remaining discount' => [ - 31, + 'Discount $50 which is more then all subtotals combined proportionally spread between products' => [ + 50, [ 'subtotal' => 10.00, - 'discount_amount' => -8.8600, + 'discount_amount' => -10.0000, 'shipping_amount' => 5.00, - 'grand_total' => 6.14, + 'grand_total' => 5.0000, ], [ 'subtotal' => 20.00, - 'discount_amount' => -17.7100, + 'discount_amount' => -20.0000, 'shipping_amount' => 5.00, - 'grand_total' => 7.29, + 'grand_total' => 5.0000, ], [ 'subtotal' => 5.00, 'discount_amount' => -5.00, 'shipping_amount' => 0.00, - 'grand_total' => 0.00, + 'grand_total' => 0.0000, ] - ] + ], ]; } + /** + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/SalesRule/_files/cart_rule_50_percent_off_no_condition.php + * @magentoDataFixture Magento/SalesRule/_files/cart_fixed_10_discount.php + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_products.php + * @return void + */ + public function testDiscountsWhenByPercentRuleAppliedFirstAndCartFixedRuleSecond(): void + { + $totalDiscount = -20.99; + $discounts = [ + 'simple1' => 5.72, + 'simple2' => 15.27, + ]; + $quote = $this->getQuote('test_quote_with_simple_products'); + $quote->setCouponCode('2?ds5!2d'); + $quote->collectTotals(); + $this->quoteRepository->save($quote); + $this->assertEquals(21.98, $quote->getBaseSubtotal()); + $this->assertEquals($totalDiscount, $quote->getShippingAddress()->getDiscountAmount()); + $items = $quote->getAllItems(); + $this->assertCount(2, $items); + $item = array_shift($items); + $this->assertEquals('simple1', $item->getSku()); + $this->assertEquals(5.99, $item->getPrice()); + $this->assertEquals($discounts[$item->getSku()], $item->getDiscountAmount()); + $item = array_shift($items); + $this->assertEquals('simple2', $item->getSku()); + $this->assertEquals(15.99, $item->getPrice()); + $this->assertEquals($discounts[$item->getSku()], $item->getDiscountAmount()); + } + /** * Get list of orders by quote id. * diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_fixed_10_discount.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_fixed_10_discount.php new file mode 100644 index 0000000000000..cc69733b37c16 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_fixed_10_discount.php @@ -0,0 +1,38 @@ +create(Rule::class); +$salesRule->setData( + [ + 'name' => '10$ fixed discount on whole cart', + 'is_active' => 1, + 'customer_group_ids' => [GroupManagement::NOT_LOGGED_IN_ID], + 'coupon_type' => Rule::COUPON_TYPE_NO_COUPON, + 'conditions' => [], + 'simple_action' => Rule::CART_FIXED_ACTION, + 'discount_amount' => 10, + 'discount_step' => 10, + 'stop_rules_processing' => 0, + 'website_ids' => [ + $objectManager->get(StoreManagerInterface::class)->getWebsite()->getId(), + ], + 'store_labels' => [ + 'store_id' => 0, + 'store_label' => '10$ fixed discount on whole cart', + ] + ] +); +$objectManager->get(RuleResourceModel::class)->save($salesRule); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_fixed_10_discount_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_fixed_10_discount_rollback.php new file mode 100644 index 0000000000000..431104c993458 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_fixed_10_discount_rollback.php @@ -0,0 +1,36 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->addFilter('name', '10$ fixed discount on whole cart') + ->create(); +/** @var RuleRepositoryInterface $ruleRepository */ +$ruleRepository = $objectManager->get(RuleRepositoryInterface::class); +$items = $ruleRepository->getList($searchCriteria) + ->getItems(); +/** @var Rule $salesRule */ +$salesRule = array_pop($items); +if ($salesRule !== null) { + $ruleRepository->deleteById($salesRule->getRuleId()); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_50_percent_off_no_condition.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_50_percent_off_no_condition.php index 77178abdb2384..de6dff7194ac3 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_50_percent_off_no_condition.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_50_percent_off_no_condition.php @@ -24,7 +24,7 @@ 'simple_action' => 'by_percent', 'discount_amount' => 50, 'discount_step' => 0, - 'stop_rules_processing' => 1, + 'stop_rules_processing' => 0, 'website_ids' => [ \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Store\Model\StoreManagerInterface::class