Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions app/code/Magento/SalesRule/Helper/CartFixedDiscount.php
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,92 @@ public function getAvailableDiscountAmount(
return $availableDiscountAmount;
}

/**
* Initial cart-fixed balance for shipping discount processing.
* Single: remaining after item allocation on quote.
* Multi: full rule amount (legacy).
*
* @param Quote $quote
* @param Rule $rule
* @param string|int $ruleId
* @param bool $isMultiShipping
* @return float
*/
public function getCartFixedShippingRuleBalance(
Quote $quote,
Rule $rule,
$ruleId,
bool $isMultiShipping
): float {
if ($isMultiShipping) {
return (float) $rule->getDiscountAmount();
}
$quoteCartRules = $quote->getCartFixedRules();
if (is_array($quoteCartRules) && array_key_exists($ruleId, $quoteCartRules)) {
return (float) $quoteCartRules[$ruleId];
}
return (float) $rule->getDiscountAmount();
}

/**
* Single-shipping only: remaining balance capped by shipping discount tax basis.
*
* @param Quote $quote
* @param float $availableRuleBalance
* @param float $shippingAmountForDiscount
* @param float $baseShippingAmountForDiscount
* @param float $appliedShippingDiscount
* @param float $baseAppliedShippingDiscount
* @return array{0: float, 1: float} [quote currency, base currency]
*/
public function calculateSingleShippingCartFixedDiscount(
Quote $quote,
float $availableRuleBalance,
float $shippingAmountForDiscount,
float $baseShippingAmountForDiscount,
float $appliedShippingDiscount,
float $baseAppliedShippingDiscount
): array {
if ($availableRuleBalance <= 0.0) {
return [0.0, 0.0];
}

$baseDiscountAmount = max(
0.0,
min(
$availableRuleBalance,
$baseShippingAmountForDiscount - $baseAppliedShippingDiscount
)
);
$discountAmount = max(
0.0,
min(
(float) $this->priceCurrency->convert($baseDiscountAmount, $quote->getStore()),
$shippingAmountForDiscount - $appliedShippingDiscount
)
);

return [$discountAmount, $baseDiscountAmount];
}

/**
* Sync remaining cart-fixed balance back onto the quote (single shipping only).
*
* @param Quote $quote
* @param string|int $ruleId
* @param float $remaining
* @return void
*/
public function syncQuoteCartFixedRuleBalance(Quote $quote, $ruleId, float $remaining): void
{
$quoteCartRules = $quote->getCartFixedRules();
if (!is_array($quoteCartRules)) {
$quoteCartRules = [];
}
$quoteCartRules[$ruleId] = $remaining;
$quote->setCartFixedRules($quoteCartRules);
}

/**
* Get configuration setting "Apply Discount On Prices Including Tax" value
*
Expand Down
131 changes: 95 additions & 36 deletions app/code/Magento/SalesRule/Model/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
* @method int getCustomerGroupId()
* @method Validator setCustomerGroupId($id)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
*/
class Validator extends \Magento\Framework\Model\AbstractModel implements ResetAfterRequestInterface
{
Expand Down Expand Up @@ -480,42 +481,13 @@ public function processShippingAmount(Address $address)
$baseDiscountAmount = $rule->getDiscountAmount();
break;
case Rule::CART_FIXED_ACTION:
$cartRules = $address->getCartFixedRules();
$quoteAmount = $this->priceCurrency->convert($rule->getDiscountAmount(), $quote->getStore());
$isAppliedToShipping = (int) $rule->getApplyToShipping();
$ruleId = $rule->getId() ?? '';
if (!isset($cartRules[$ruleId])) {
$cartRules[$ruleId] = $rule->getDiscountAmount();
}
if ($cartRules[$ruleId] > 0) {
$shippingQuoteAmount = (float) $address->getShippingAmount();
$quoteBaseSubtotal = (float) $quote->getBaseSubtotal();
$isMultiShipping = $this->cartFixedDiscountHelper->checkMultiShippingQuote($quote);
if ($isAppliedToShipping) {
$quoteBaseSubtotal = ($quote->getIsMultiShipping() && $isMultiShipping) ?
$this->cartFixedDiscountHelper->getQuoteTotalsForMultiShipping($quote) :
$this->cartFixedDiscountHelper->getQuoteTotalsForRegularShipping(
$address,
$quoteBaseSubtotal,
$shippingQuoteAmount
);
$discountAmount = $this->cartFixedDiscountHelper->
getShippingDiscountAmount(
$rule,
$shippingQuoteAmount,
$quoteBaseSubtotal
);
$baseDiscountAmount = $discountAmount;
} else {
$discountAmount = min($shippingQuoteAmount, $quoteAmount);
$baseDiscountAmount = min(
$baseShippingAmount - $address->getBaseShippingDiscountAmount(),
$cartRules[$ruleId]
);
}
$cartRules[$ruleId] -= $baseDiscountAmount;
}
$address->setCartFixedRules($cartRules);
[$discountAmount, $baseDiscountAmount] = $this->processCartFixedShippingAmount(
$address,
$quote,
$rule,
(float) $shippingAmount,
(float) $baseShippingAmount
);
break;
case Rule::BUY_X_GET_Y_ACTION:
$allQtyDiscount = $this->getDiscountQtyAllItemsBuyXGetYAction($quote, $rule);
Expand Down Expand Up @@ -562,6 +534,93 @@ public function processShippingAmount(Address $address)
return $this;
}

/**
* Apply cart-fixed rule discount to shipping for one address.
*
* Single shipping: remaining quote balance capped by shipping tax-basis amounts.
* Multi shipping: legacy proportional share of full rule amount.
*
* @param Address $address
* @param Quote $quote
* @param Rule $rule
* @param float $shippingAmount Shipping amount for discount (quote currency)
* @param float $baseShippingAmount Shipping amount for discount (base currency)
* @return array{0: float, 1: float} [quote currency discount, base currency discount]
*/
private function processCartFixedShippingAmount(
Address $address,
Quote $quote,
Rule $rule,
float $shippingAmount,
float $baseShippingAmount
): array {
$discountAmount = 0.0;
$baseDiscountAmount = 0.0;
$cartRules = $address->getCartFixedRules();
if (!is_array($cartRules)) {
$cartRules = [];
}
$quoteAmount = $this->priceCurrency->convert($rule->getDiscountAmount(), $quote->getStore());
$isAppliedToShipping = (int) $rule->getApplyToShipping();
$ruleId = $rule->getId() ?? '';
$isMultiShipping = $quote->getIsMultiShipping()
&& $this->cartFixedDiscountHelper->checkMultiShippingQuote($quote);

if (!isset($cartRules[$ruleId])) {
$cartRules[$ruleId] = $this->cartFixedDiscountHelper->getCartFixedShippingRuleBalance(
$quote,
$rule,
$ruleId,
$isMultiShipping
);
}
if ($cartRules[$ruleId] > 0) {
$shippingQuoteAmount = (float) $address->getShippingAmount();
if ($isAppliedToShipping) {
if ($isMultiShipping) {
// HEAD multi path: proportional share of full rule.
$quoteBaseSubtotal = $this->cartFixedDiscountHelper
->getQuoteTotalsForMultiShipping($quote);
$discountAmount = $this->cartFixedDiscountHelper->getShippingDiscountAmount(
$rule,
$shippingQuoteAmount,
$quoteBaseSubtotal
);
$baseDiscountAmount = $discountAmount;
} else {
// Single-ship: remaining balance + tax-basis cap.
[$discountAmount, $baseDiscountAmount] = $this->cartFixedDiscountHelper
->calculateSingleShippingCartFixedDiscount(
$quote,
(float) $cartRules[$ruleId],
$shippingAmount,
$baseShippingAmount,
(float) $address->getShippingDiscountAmount(),
(float) $address->getBaseShippingDiscountAmount()
);
}
} else {
// HEAD else branch (dead when apply-to-shipping is filtered above; keep for BC).
$discountAmount = min($shippingQuoteAmount, $quoteAmount);
$baseDiscountAmount = min(
$baseShippingAmount - $address->getBaseShippingDiscountAmount(),
$cartRules[$ruleId]
);
}
$cartRules[$ruleId] -= $baseDiscountAmount;
}
$address->setCartFixedRules($cartRules);
if (!$isMultiShipping) {
$this->cartFixedDiscountHelper->syncQuoteCartFixedRuleBalance(
$quote,
$ruleId,
(float) ($cartRules[$ruleId] ?? 0)
);
}

return [(float) $discountAmount, (float) $baseDiscountAmount];
}

/**
* Calculate quote totals for each rule and save results
*
Expand Down
Loading