Catalog And Shopping Cart Pricing Rules Stacking

Ran into an issue today that I thought I would share a fix for, having catalog and shopping cart pricing rules stack their discount on each other. What do I mean?

Say you have a catalog price rule that gives a 25% discount to all items in a certain category so a product with a base price of $20 would display with a discount to $15. Now, you have a coupon code for 30% off any purchase setup as a shopping cart price rule in the admin. What happens when a user adds this item to their cart and applies the coupon code? With default Magento it runs as follows:

  1. Item base price $20 gets the 25% discount from the catalog rule and becomes $15
  2. Coupon code rule gets applied but applies to the currently discounted $15 price
  3. Total after discounts becomes $10

Now, this is not what was expected as we had the “Stop Further Rules Processing” set to “Yes” and the priority of the shopping cart price rule lower than the catalog price rule. What we wanted to happen was the customer would just get the 30% discount applied to the original price of the product and become $13.33. So why does this happen?

Analyzing the core Magento code, we see the following inside of app/code/core/Mage/SalesRule/Model/Validator.php:process()

$itemPrice              = $this->_getItemPrice($item);
$baseItemPrice          = $this->_getItemBasePrice($item);
$itemOriginalPrice      = $this->_getItemOriginalPrice($item);
$baseItemOriginalPrice  = $this->_getItemBaseOriginalPrice($item);
...
$discountAmount    = ($qty*$itemPrice - $item->getDiscountAmount()) * $_rulePct;
$baseDiscountAmount= ($qty*$baseItemPrice - $item->getBaseDiscountAmount()) * $_rulePct;
//get discount for original price
$originalDiscountAmount    = ($qty*$itemOriginalPrice - $item->getDiscountAmount()) * $_rulePct;
$baseOriginalDiscountAmount= ($qty*$baseItemOriginalPrice - $item->getDiscountAmount()) * $_rulePct;

The problem with this is that the item’s price is the already discounted price from the catalog price rule. This is what ends up causing it to stack the pricing rules even though you do not want it to.

There is no easy way to change this either, the solution I came up with for my current project was to instead have it apply only the best discount to the item. This solution worked best for the client since they would prefer to only have the best discount apply and not have anything stack anyways.

I accomplished this by extending this class into a module and overriding the process() function. First, create a new model file in one of your modules and have it extend the SalesRule Validator class:

class MyCompany_MyModule_Model_Validator extends Mage_SalesRule_Model_Validator {}

Now copy the process() method into your class and adjust lines 317-321 (the 4 discount variables being set in the second part of the code above) to be as follows:

if ($itemOriginalPrice != $item->getProduct()->getPrice())
{
    $catalogDiscountPercent = round(($item->getProduct()->getPrice() - $itemOriginalPrice) / $item->getProduct()->getPrice(), 4);
    $_rulePct               = max(0, $_rulePct - $catalogDiscountPercent);
    $discountAmount         = ($qty * $item->getProduct()->getPrice() - $item->getDiscountAmount()) * $_rulePct;
    $baseDiscountAmount     = ($qty * $item->getProduct()->getPrice() - $item->getBaseDiscountAmount()) * $_rulePct;
    //get discount for original price
    $originalDiscountAmount     = ($qty * $item->getProduct()->getPrice() - $item->getDiscountAmount()) * $_rulePct;
    $baseOriginalDiscountAmount = ($qty * $item->getProduct()->getPrice() - $item->getDiscountAmount()) * $_rulePct;
}
else
{
    $discountAmount     = ($qty * $itemPrice - $item->getDiscountAmount()) * $_rulePct;
    $baseDiscountAmount = ($qty * $baseItemPrice - $item->getBaseDiscountAmount()) * $_rulePct;
    //get discount for original price
    $originalDiscountAmount     = ($qty * $itemOriginalPrice - $item->getDiscountAmount()) * $_rulePct;
    $baseOriginalDiscountAmount = ($qty * $baseItemOriginalPrice - $item->getDiscountAmount()) * $_rulePct;
}

Now we just need to tell Magento to use this class instead of the default by adding the following to our modules config.xml:

<models>
    <salesrule>
        <rewrite>
            <validator>MyCompany_MyModule_Model_Validator</validator>
        </rewrite>
    </salesrule>
</models>

Now refresh your Magento’s cache and it should begin using our new override model file when processing the rules. So how does this work?

The first thing we did was check to see if the current item price is different from the product’s base price. If it differs, that means there is some catalog rule being applied to the product already and discounting its price so we go ahead and calculate what that percentage discount is from the catalog rule. Next, we subtract that percentage from the shopping cart price rule to see if we are already getting a better discount from the catalog price rule or not.

If the shopping cart price rule is a better discount than the catalog price rule, we apply the difference in percent between the catalog and shopping cart price rule to the base product price to get the difference in discount amounts and set that to the item’s discount amount. We have to use the difference in percent because the rest of the system (totals, payment processing, etc) is using the discounted price from the catalog rule for the item’s price and not the base product’s price.

If the catalog rule is as good or better than the shopping cart discount, it will see the shopping cart rule’s percent as 0 and not adjust the current price at all.

If the item’s price and product price is the same, it uses the default Magento behavior to determine the discount amount.

This was the solution I found to work best for this client and project, if you have any enhancements to this feel free to leave a comment and let me know.

6 responses to “Catalog And Shopping Cart Pricing Rules Stacking”

  1. This is really interesting. It would seem that the approach is a little naive as it is not taking into account other cases of scpecial price (tier prices, custom options, configurable options, bundle options, group prices and special price off the top of my head) and assumes that the cart rule is a percent of product price, but overall is a good starting point to tinker with this rule overlapping issue

    • Those different price setups for the product are already taken care of with the product->getPrice() call. That will return the price based on those rules (at least in the case of group, tier and special prices) and compare it to the price of the item that was added into the cart (which should equal that same getPrice() unless a catalog rule applied a discount) and then check to see if the cart or the catalog rule gives a better discount.

      As for custom options and configurable products, you may be correct in that. The client does not use either of those for their products so we did not test the adjustments against those conditions.

      As for assuming the rule is percent, the section we are modifying is specifically for the “By Percent” action. There are other sections inside the switch statement for each of the other discount types that we are not touching with this update so they should still function the same as default Magento.

      • There is one bug for this patch when there are mutli currencies,ie. the second currency is eup pounds and the original currency is US dollar.After selected EUP Pounds,the value for the paramters value are(the product original price is $28.24,and there is special price as $22.89):
        itemPrice==>€16.560915
        baseItemPrice==>$22.89
        itemOriginalPrice==>16.56
        baseItemOriginalPrice==>$22.89

        for itemOriginalPrice==>16.56
        for item->getProduct()->getPrice ==>$28.2400
        As the program in the article,

        $itemOriginalPrice != $item->getProduct()->getPrice() it would be wrong for it compares with different currencies’ price.
        I thing it would be
        $baseItemOriginalPrice != $item->getProduct()->getPrice()

        • Good catch, you would probably need to add a setStore() call when getting the product price so it gets it for the current store currency. Or, if you have multiple currencies in the same store, you would need to convert it between currencies so that the prices being compared are all in the same currency.

Leave a Reply

Your email address will not be published.