Magento Shopping Cart Price Rules

The functionality behind Magento shopping cart price rules is within the Mage_SalesRule module.

Shopping cart price rules can be configured within Magento in the admin under Promotions -> Shopping Cart Price Rules. These rules differ from the catalog rules as they apply to the quote, whereas catalog rules apply to the catalog information about the product entities.

Magento uses a total model to calculate the discount by shopping cart rules and shows it in the cart total. This discount total is calculated after the subtotal and shipping and before the grand total, as represented in the Mage_SalesRule module’s config.xml file.

<?xml version="1.0"?>
<config>
    <global>
        <sales>
            <quote>
                <totals>
                    <freeshipping>
                        <class>salesrule/quote_freeshipping</class>
                        <after>subtotal</after>
                        <before>tax_subtotal,shipping</before>
                    </freeshipping>
                    <discount&gt;
                        <class>salesrule/quote_discount</class>
                        <after>subtotal,shipping</after>
                        <before>grand_total</before>
                    </discount>
                </totals>
            </quote>
        </sales>
    </global>
</config>

You’ll also notice that there is a freeshipping total also defined here. This is because within shopping cart price rules you can choose to enable free shipping should a customer match a rule.

The discount total has its own model, as represented by the class nodes, within Mage_SalesRule/Model/Quote/Discount.php.

public function collect(Mage_Sales_Model_Quote_Address $address)
{
    parent::collect($address);
    $quote = $address->getQuote();
    $store = Mage::app()->getStore($quote->getStoreId());
    $this->_calculator->reset($address);

    $items = $this->_getAddressItems($address);
    if (!count($items)) {
        return $this;
    }

    $eventArgs = array(
        '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(array());
    $items = $this->_calculator->sortItemsByPriority($items);
    foreach ($items as $item) {
        if ($item->getNoDiscount()) {
            $item->setDiscountAmount(0);
            $item->setBaseDiscountAmount(0);
        }
        else {
           /**
             * Child item discount we calculate for parent
             */
            if ($item->getParentItemId()) {
                continue;
            }

            $eventArgs['item'] = $item;
            Mage::dispatchEvent('sales_quote_address_discount_item', $eventArgs);

            if ($item->getHasChildren() && $item->isChildrenCalculated()) {
                foreach ($item->getChildren() as $child) {
                    $this->_calculator->process($child);
                    $eventArgs['item'] = $child;
                    Mage::dispatchEvent('sales_quote_address_discount_item', $eventArgs);

                    $this->_aggregateItemDiscount($child);
                }
            } else {
                $this->_calculator->process($item);
                $this->_aggregateItemDiscount($item);
            }
        }
    }

    /**
     * process weee amount
     */
    if (Mage::helper('weee')->isEnabled() && Mage::helper('weee')->isDiscounted($store)) {
        $this->_calculator->processWeeeAmount($address, $items);
    }

    /**
     * Process shipping amount discount
     */
    $address->setShippingDiscountAmount(0);
    $address->setBaseShippingDiscountAmount(0);
    if ($address->getShippingAmount()) {
        $this->_calculator->processShippingAmount($address);
        $this->_addAmount(-$address->getShippingDiscountAmount());
        $this->_addBaseAmount(-$address->getBaseShippingDiscountAmount());
    }

    $this->_calculator->prepareDescription($address);
    return $this;
}

The process() method belongs in a Validator.php model file that actually checks the ‘action’ of the shopping cart price rule(s) and applies the discount.

<?php
class Mage_SalesRule_Model_Validator extends Mage_Core_Model_Abstract {
    public function process() {
        ....
        foreach ($this->_getRules() as $rule) {
            ....
            switch ($rule->getSimpleAction()) {
                case Mage_SalesRule_Model_Rule::TO_PERCENT_ACTION:
                    $rulePercent = max(0, 100-$rule->getDiscountAmount());
                //no break;
                case Mage_SalesRule_Model_Rule::BY_PERCENT_ACTION:
                    $step = $rule->getDiscountStep();
                    if ($step) {
                        $qty = floor($qty/$step)*$step;
                    }
                    $_rulePct = $rulePercent/100;
                    $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;

                    if (!$rule->getDiscountQty() || $rule->getDiscountQty()>$qty) {
                        $discountPercent = min(100, $item->getDiscountPercent()+$rulePercent);
                        $item->setDiscountPercent($discountPercent);
                    }
                    break;
                case Mage_SalesRule_Model_Rule::TO_FIXED_ACTION:
                    $quoteAmount = $quote->getStore()->convertPrice($rule->getDiscountAmount());
                    $discountAmount    = $qty * ($itemPrice-$quoteAmount);
                    $baseDiscountAmount = $qty * ($baseItemPrice-$rule->getDiscountAmount());
                    //get discount for original price
                    $originalDiscountAmount    = $qty * ($itemOriginalPrice-$quoteAmount);
                    $baseOriginalDiscountAmount = $qty * ($baseItemOriginalPrice-$rule->getDiscountAmount());
                    break;

                case Mage_SalesRule_Model_Rule::BY_FIXED_ACTION:
                    $step = $rule->getDiscountStep();
                    if ($step) {
                        $qty = floor($qty/$step)*$step;
                    }
                    $quoteAmount        = $quote->getStore()->convertPrice($rule->getDiscountAmount());
                    $discountAmount     = $qty * $quoteAmount;
                    $baseDiscountAmount = $qty * $rule->getDiscountAmount();
                    break;

                case Mage_SalesRule_Model_Rule::CART_FIXED_ACTION:
                    ....
            }
            ....
        }
        ....
    }
    ....
}

The model that is responsible for generating coupon codes is the Mage_SalesRule_Model_Coupon_Codegenerator model.

<?php
class Mage_SalesRule_Model_Coupon_Codegenerator extends Varien_Object
    implements Mage_SalesRule_Model_Coupon_CodegeneratorInterface
{
    /**
     * Retrieve generated code
     *
     * @return string
     */
    public function generateCode()
    {
        $alphabet = ($this->getAlphabet() ? $this->getAlphabet() : 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
        $lengthMin = ($this->getLengthMin() ? $this->getLengthMin() : 16);
        $lengthMax = ($this->getLengthMax() ? $this->getLengthMax() : 32);
        $length = ($this->getLength() ? $this->getLength() : rand($lengthMin, $lengthMax));
        $result = '';
        $indexMax = strlen($alphabet) - 1;
        for ($i = 0; $i < $length; $i++) {
            $index = rand(0, $indexMax);
            $result .= $alphabet{$index};
        }
        return $result;
    }
    ....
}

When the sales_order_place_after event is dispatched in the Mage_Sales_Model_Order class, an observer within the Mage_SalesRule module increments the times_used column that tracks the rule’s and customer’s discount usage.

public function sales_order_afterPlace($observer)
{
    $order = $observer->getEvent()->getOrder();

    if (!$order) {
        return $this;
    }

    // lookup rule ids
    $ruleIds = explode(',', $order->getAppliedRuleIds());
    $ruleIds = array_unique($ruleIds);

    $ruleCustomer = null;
    $customerId = $order->getCustomerId();

    // use each rule (and apply to customer, if applicable)
    if ($order->getDiscountAmount() != 0) {
        foreach ($ruleIds as $ruleId) {
            if (!$ruleId) {
                continue;
            }
            $rule = Mage::getModel('salesrule/rule');
            $rule->load($ruleId);
            if ($rule->getId()) {
                $rule->setTimesUsed($rule->getTimesUsed() + 1);
                $rule->save();

                if ($customerId) {
                    $ruleCustomer = Mage::getModel('salesrule/rule_customer');
                    $ruleCustomer->loadByCustomerRule($customerId, $ruleId);

                    if ($ruleCustomer->getId()) {
                        $ruleCustomer->setTimesUsed($ruleCustomer->getTimesUsed() + 1);
                    }
                    else {
                        $ruleCustomer
                        ->setCustomerId($customerId)
                        ->setRuleId($ruleId)
                        ->setTimesUsed(1);
                    }
                    $ruleCustomer->save();
                }
            }
        }
        $coupon = Mage::getModel('salesrule/coupon');
        /** @var Mage_SalesRule_Model_Coupon */
        $coupon->load($order->getCouponCode(), 'code');
        if ($coupon->getId()) {
            $coupon->setTimesUsed($coupon->getTimesUsed() + 1);
            $coupon->save();
            if ($customerId) {
                $couponUsage = Mage::getResourceModel('salesrule/coupon_usage');
                $couponUsage->updateCustomerCouponTimesUsed($customerId, $coupon->getId());
            }
        }
    }
}

Key things to remember about shopping cart price rules:

  • Only one coupon code can be applied at once
  • For orders created within the admin, shopping cart price rules can be disabled on specific items but they are always applied on all items in the frontend.

Magento stores the shopping cart price rule data in the following database tables.

  • salesrule – Contains generic information about the rule, including serialised conditions and actions data
  • salesrule_coupon – Contains information about the coupon codes used for a rule
  • salesrule_coupon_usage – Contains information about the usage of coupons when orders are placed
  • salesrule_customer – Contains information about the usage of a rule for a customer such as the times_used column
  • salesrule_customer_group – Contains information about customer groups assigned to a rule
  • salesrule_label – Contains information about the label used to name the rule
  • salesrule_product_attribute – Contains information such as the attribute ID used in the rule conditions
  • salesrule_website – Contains the website ID(s) that the rule is assigned to

Note: This article is based on Magento Community/Open Source version 1.9.