How to detect if a price rule gets applied to a product

We were asked to write a simple module that makes it possible to display a rule-specific teaser on product pages when they are affected by catalog price rules.

So, our first question was: how can we even detect if a price rule is applied to a product?

The easiest Solutions

Model Mage_CatalogRule_Model_Rule offers two methods that seem quite interesting: loadProductRules and calcProductPriceRule.

loadProductRules will save any rule ids that are attached to the given product in the product’s collection:

// $_product is an instance of Mage_Catalog_Model_Product
$catalogRule = Mage::getModel('catalogrule/rule')->loadProductRules($_product);

$catalogRule will now contain an array: Array( [rule_id] => [irrelevant_number] ). If you want to know why we get an irrelevant number here, you can read up below.

While this is the fastest and easiest way to get any rules applied to our product, the most important thing to note is that it literally gives us *every* rule that is somehow connected. There are no filters applied which means we also get future and past rules and rules that are meant for any customer group or website. Depending on what you want to do this can be totally fine, of course.

Lets take a look at calcProductPriceRule. This handy method is returning our product’s price with any catalog rule applied to it. If there is no rule applied it will return NULL. This sounds great because we can use it do exactly what we need – detect whether a price rule gets applied to our product or not:

if( Mage::getModel('catalogrule/rule')->calcProductPriceRule($_product,$_product->getPrice()) ){
	echo 'Catalog price rule applied';
} else {
	echo 'No catalog price rule applied';
}

And yes, this one filters our rules by date, customer groups and website. The only downside of it is that it does more than we need it to do – which means it takes more resources than necessary.

So what are the alternatives?

An alternative way and also the way that we ended up going is reproducing what calcProductPriceRule does, but without actually calculating any prices. Essentially, it makes use of Mage_CatalogRule_Model_Resource_Rule::getRulesFromProduct and so will we:

Step 1: Set up your Model

class My_Module_Model_Catalogrule extends Mage_Core_Model_Abstract {
  /*
  * @param Mage_Catalog_Model_Product $product
  * @return array
  */
  public function getRulesFromProduct(Mage_Catalog_Model_Product $product)
  {
	  $productId  = $product->getId();
	  $storeId    = $product->getStoreId();
	  $websiteId  = Mage::app()->getStore($storeId)->getWebsiteId();
	  if ($product->hasCustomerGroupId()) {
		  $customerGroupId = $product->getCustomerGroupId();
	  } else {
		  $customerGroupId = Mage::getSingleton('customer/session')->getCustomerGroupId();
	  }
	  $dateTs     = Mage::app()->getLocale()->date()->getTimestamp();
  
	  return Mage::getResourceModel('catalogrule/rule')->getRulesFromProduct($dateTs, $websiteId, $customerGroupId, $productId);	  
  }	
}

Our method returns an empty array if the product does not have any price rules applied.

Step 2: Set up your Block

class My_Module_Block_Catalog_Product_Catalogrule extends Mage_Catalog_Block_Product_Abstract {
	
	protected $_rules;
	public function getRules(){
		if( is_null($this->_rules) ){
			$catalogrule = Mage::getModel('my_module/catalogrule');
			if(($product = $this->getProduct()) && ($rules = $catalogrule->getRulesFromProduct($product))){
				$this->_rules = $rules;
			} else {
				$this->_rules = false;	
			}
		}
		return $this->_rules;
	}
	
	public function hasRuleApplied(){
		if( $this->getRules() ){
			return true;
		}
		return false;
	}	
}

Step 3: Set up your template

Now we can do stuff like this in our block’s template:

<?php if($this->hasRuleApplied()): ?>
    <?php foreach($this->getRules() as $rule): ?>
    <div class="pricerule-teaser">
		<strong><?php 
        switch($rule['action_operator']){
            case 'by_percent':
                echo $this->__('%s%% Discount', round($rule['action_amount']));
            break;
            case 'by_fixed':
                echo $this->__('%s Discount', Mage::helper('core')->currency($rule['action_amount'], true, false));
            break;	
            case 'to_percent':
                echo $this->__('Only %s%% of the original price', round($rule['action_amount']));
            break;	
            case 'to_fixed':
                echo $this->__('Only %s', Mage::helper('core')->currency($rule['action_amount'], true, false));
            break;	
        } ?></strong>
    	<?php echo $this->__('until the %s !', date('dS M', $rule['to_time'])) ?>
    </div>
    <?php endforeach; ?>
<?php endif; ?>

Read up below if you want a list of all the values our $rule array stores.

Well and that’s it! Let us know if you have questions or ideas for improvement.

Continue reading How to detect if a price rule gets applied to a product

Adding Module pages to your Magento Sitemap

In earlier Magento versions it wasn’t possible to add additionals entries to your Magento Sitemap without overriding or extending Mage_Sitemap_Model_Sitemap.

Luckily, with the introduction of two events, sitemap_categories_generating_before & sitemap_products_generating_before, this became possible and therefore way simpler, too. Unless you don’t have to extend Mage_Sitemap_Model_Sitemap anyway, for example because you want to add images to your sitemap, you rather want to use these events.

Good to know…

Which event you want to choose depends on the changefreq and priority values you’d prefer for your additional page entries. You can look them up or change them in your Magento Sitemap configurations under System > Configuration > Catalog > Google Sitemap.

Even tho Magento is adding entries for category-, product- and CMS-pages, it does not fire an event for the latter one. Hence you have to choose between your category & product sitemap values.

In our example we prefer the product sitemap values. Therefore we go with the event sitemap_products_generating_before.

To it then!

Lets assume our custom module creates dynamic “News” pages and stores the page information, including the page URLs, within our model database table.

So first we add an event listener to the <global> tag in our module’s config.xml:

    <events>
        <sitemap_products_generating_before>
            <observers>
                <{your_unique_event_observer_name}>
                    <class>{Brand}_{Module}_Model_Observer</class>
                    <method>addPagesToSitemap</method>
                </{your_unique_event_observer_name}>
            </observers>
        </sitemap_products_generating_before>
    </events>

Then, in our module’s Observer, we let the method addPagesToSitemap add extra items to the product collection:

<?php
class {Brand}_{Module}_Model_Observer
{   
    function addPagesToSitemap(Varien_Event_Observer $observer){

        $sitemapItems = $observer->getEvent()->getCollection()->getItems();

        // Get your module's page collection including their urls
        // Adjust the following lines to your needs
        $collection = Mage::getModel('{brand}_{module}/pages')->getCollection()
            ->addFieldToSelect(array('page_id','url'))
            ->addFieldToFilter('display', 1);
        // My module stores the page path separately,
        // you might don't need this:
        $modulePagePath = Mage::helper('{my-modules-helper}')->getNewsPath();

        foreach($collection as $_item){
            $varienObject = new Varien_Object();
            // We don't want to override 
            // any existing product/category items
            $uniqueId = '{module}'.$_item->getPageId();         
            $varienObject->setId($uniqueId);

            // You might want to adjust this if your item
            // stores the complete url. Don't add the base url tho,
            // Mage_Sitemap_Model_Sitemap::generateXml adds it
            $varienObject->setUrl($modulePagePath . DS . $_item->getUrl());

            $sitemapItems[$uniqueId] = $varienObject;
        }
        $observer->getEvent()->getCollection()->setItems($sitemapItems);

        return $this;
    }
}

Well and that’s it! You can test it by manually generating your Magento Sitemap under Catalog > Google Sitemap.


(Tested in Magento 1.9.2.2)