I recently stumbled upon a bug in Magento 1.4.2.0 which hasn’t been covered online yet, and causes catalog rules to break sometimes. The “sometimes” part has to do with the order in which you created the rules, or more specifically the last rule inserted into the database is the one actually working. All other rules won’t get applied when saving the product.

The only way out of this, is to open each rule and save and apply it one at a time. Hitting the “Apply All Rules” button doesn’t work.

The Cause

After saving a product (new or existing) an event fires triggering the Mage_CatalogRule_Model_Observer::applyAllRulesOnProduct function.

    public function applyAllRulesOnProduct($observer)
    {
        $product = $observer->getEvent()->getProduct();
        if ($product->getIsMassupdate()) {
            return;
        }

        $productWebsiteIds = $product->getWebsiteIds();

        $rules = Mage::getModel('catalogrule/rule')->getCollection()
            ->addFieldToFilter('is_active', 1);

        foreach ($rules as $rule) {
            if (!is_array($rule->getWebsiteIds())) {
                $ruleWebsiteIds = (array)explode(',', $rule->getWebsiteIds());
            } else {
                $ruleWebsiteIds = $rule->getWebsiteIds();
            }
            $websiteIds = array_intersect($productWebsiteIds, $ruleWebsiteIds);
            $rule->applyToProduct($product, $websiteIds);
        }
        return $this;
    }

This function is responsible for applying all the rules to the single product you just edited. In this function, all rules get loaded and applied to the product one at a time.

Now, when we take a look at the Mage_CatalogRule_Model_Mysql4_Rule::applyToProduct function, there’s a lot going on.

  1. The catalogrule_product table loses all records related to your product and the current rule.
  2. If the rule is not valid, the catalogrule_product_price table loses all records related to your product.

Now this is a problem! All rule prices get dropped, regardless of the other (good) rules, because the current rule is invalid. I was in fact amazed to find this kind of poor logic, but hey, we’ll fix it in a minute :p So, here you have it. What will happen in reality goes like this:

  1. Processing rule A -> Invalid -> Delete all rows for the product
  2. Processing rule B -> Invalid -> Delete all rows for the product
  3. Processing rule C -> Valid -> Writing new prices
  4. Processing rule D -> Invalid -> Delete all rows for the product

So rule C did run, but it’s hard work got deleted immediately after. Depending on how the rules are ordered in your database (table catalogrule), you may actually see some products discounted, given that rule C was your last one.

The Solution

Now, I’ve given this a lot of thought, how to best deal with this problem, and not needing to change too much core code. This is what I can up with, and it will work just fine on Magento 1.4.2.0. First of all, we won’t be editing Mage_CatalogRule_Model_Mysql4_Rule, but instead create a new model that is a replacement for the observer that started it all: Mage_CatalogRule_Model_Observer.

Instead of applying all rules to the product, I will first check which rules actually apply to the product, and then only apply those. Now, there’s one thing to keep in mind here: If all rules are valid, the catalogrule_product_price table will never get reset for the specific product, so this we’ll have to do first. After that, when we apply the right rules, it will get filled up and nothing will be lost again.

Here’s my finished code with comments:

public function applyAllRulesOnProduct($observer) {
		$product = $observer->getEvent ()->getProduct ();
		/* @var $product Mage_Catalog_Model_Product */

		if ($product->getIsMassupdate ()) {
			return;
		}

		$productWebsiteIds = $product->getWebsiteIds ();

		$rules = Mage::getModel ( 'catalogrule/rule' )->getCollection ()->addFieldToFilter ( 'is_active', 1 );

		// WOUTER - WE ARE DOING THIS DIFFERENTLY, BECAUSE MAGENTO'S OWN CODE DOESN'T WORK

		// 1. Delete all rule prices
		$write = Mage::getSingleton ( 'core/resource' )->getConnection ( 'core_write' );
		/* @var $write Varien_Db_Adapter_Pdo_Mysql */

		$tableName = Mage::getSingleton ( 'core/resource' )->getTableName ( 'catalogrule/rule_product_price' );
		$write->delete ( $tableName, array ($write->quoteInto ( 'product_id=?', $product->getId() ) ) );

		foreach ( $rules as $rule ) {
			/* @var $rule Mage_CatalogRule_Model_Mysql4_Rule */

			// 2. Find the rules that are valid
			if ($rule->getConditions ()->validate ( $product )) {

				// 3. Apply the valid rules
				if (! is_array ( $rule->getWebsiteIds () )) {
					$ruleWebsiteIds = ( array ) explode ( ',', $rule->getWebsiteIds () );
				} else {
					$ruleWebsiteIds = $rule->getWebsiteIds ();
				}
				$websiteIds = array_intersect ( $productWebsiteIds, $ruleWebsiteIds );
				$rule->applyToProduct ( $product, $websiteIds );
			}
		}
		return $this;
	}

So, that’s it. You can now save products and have the right rules applied immediately!

Liked my solution or need further help? I’m free for hire.