Magento project I'm currently in charge for has had the catalog price rules not sticking issue for some time now. This issue has been first reported as catalog price rules being unapplied on their own without any obvious pattern. Since, due to nature of it's catalog, catalog price rules are very backbone of the store in question, situation where prices change on their own was certainly problematic. Even though this Magento project wasn't of this edition or version, it was using Mage_Catalogrule module that was shipped with Magento Community Edition 1.8.0.0. Later research revealed that this issue has been fixed in Magento CE 1.8.1.0 (and all Magento editions and version derived from it), but immediate upgrade wasn't viable alternative at the time because I didn't know what causes this issue and has it been fixed upstream. Allow me to share debug path I had to take to get to the bottom of this.
What I discovered after some debugging is that this issue is caused by one of the functions that apply catalog price rules after product save, but only if "Use Flat Catalog Product" is enabled in Magento admin. Logic works in a way that product is removed from both catalogrule_product
and catalogrule_product_price
tables each time product is saved. Product then gets matched against all active rules and if rule conditions that keep it under specific catalog rules weren't changed, product is inserted back into catalogrule_product and catalogrule_product_price tables. Code that removes items from catalog rule tables was working correctly, and code that inserts single product matched against catalog rule into catalog rule tables was faulty. In combination with very high API activity on this project (frequent product saves), this caused behavior where rules appear to be unapplied on single products with no apparent reason. Here are a few code snippets that led me to preceding conclusions.
First you have definition of observer for catalog_product_save_commit_after
event to apply all rules to product after product has been saved:
<?xml version="1.0"?> <!-- app/code/core/Mage/CatalogRule/etc/config.xml --> <config> <!-- Other code --> <adminhtml> <!-- Other code --> <events> <!-- Other code --> <catalog_product_save_commit_after> <observers> <catalogrule> <class>catalogrule/observer</class> <method>applyAllRulesOnProduct</method> </catalogrule> </observers> </catalog_product_save_commit_after> <!-- Other code --> </events> </adminhtml> </config> |
Next, here's an excerpt from catalogrule/observer
model containing applyAllRulesOnProduct()
function definition:
<?php // app/code/core/Mage/CatalogRule/Model/Observer.php /** * Catalog Price rules observer model */ class Mage_CatalogRule_Model_Observer { // Other code /** * Apply all catalog price rules for specific product * * @param Varien_Event_Observer $observer * @return Mage_CatalogRule_Model_Observer */ public function applyAllRulesOnProduct($observer) { $product = $observer->getEvent()->getProduct(); if ($product->getIsMassupdate()) { return; } Mage::getModel('catalogrule/rule')->applyAllRulesToProduct($product); return $this; } // Other code } |
This function calls applyAllRulesToProduct()
from catalogrule/rule model, which at some point triggers foreach to apply all matched rules to product using applyToProduct()
function from catalogrule/resource_rule model:
<?php // app/code/core/Mage/CatalogRule/Model/Resource/Rule.php /** * Catalog rules resource model * * @category Mage * @package Mage_CatalogRule * @author Magento Core Team <core@magentocommerce.com> */ class Mage_CatalogRule_Model_Resource_Rule extends Mage_Rule_Model_Resource_Abstract { // Other code /** * Apply catalog rule to product * * @param Mage_CatalogRule_Model_Rule $rule * @param Mage_Catalog_Model_Product $product * @param array $websiteIds * * @throws Exception * @return Mage_CatalogRule_Model_Resource_Rule */ public function applyToProduct($rule, $product, $websiteIds) { if (!$rule->getIsActive()) { return $this; } $ruleId = $rule->getId(); $productId = $product->getId(); $write = $this->_getWriteAdapter(); $write->beginTransaction(); $this->cleanProductData($ruleId, array($productId)); if (!$this->validateProduct($rule, $product, $websiteIds)) { $write->delete($this->getTable('catalogrule/rule_product_price'), array( $write->quoteInto('product_id = ?', $productId), )); $write->commit(); return $this; } try { $this->insertRuleData($rule, $websiteIds, array( $productId => array_combine(array_values($websiteIds), array_values($websiteIds)))); } catch (Exception $e) { $write->rollback(); throw $e; } $write->commit(); return $this; } // Other code } |
In the preceding code listing we see that applyToProduct()
function calls insertRuleData() after product has been removed from catalogrule_product
and catalogrule_product_price
tables. Next, we have code excerpt from insertRuleData()
containing the actual bug:
<?php // app/code/core/Mage/CatalogRule/Model/Resource/Rule.php /** * Catalog rules resource model * * @category Mage * @package Mage_CatalogRule * @author Magento Core Team <core@magentocommerce.com> */ class Mage_CatalogRule_Model_Resource_Rule extends Mage_Rule_Model_Resource_Abstract { // Other code /** * Inserts rule data into catalogrule/rule_product table * * @param Mage_CatalogRule_Model_Rule $rule * @param array $websiteIds * @param array $productIds */ public function insertRuleData(Mage_CatalogRule_Model_Rule $rule, array $websiteIds, array $productIds = array()) { // Other code if (count($productIds) > 0) { // WRONG: // $selectByStore->where('p.entity_id IN (?)', $productIds); // CORRECT: $selectByStore->where('p.entity_id IN (?)', array_keys($productIds)); } // Other code } // Other code } |
As you can see from previous code listings, $productIds
variable is actually twodimensional array in form of
array (size=1) 45746 => array (size=1) 1 => string '1' (length=1) |
and it's usage as second parameter for Varien_Db_Select::where()
isn't correct. Incorrect parameter causes WHERE p.entity_id IN ('1')
instead of WHERE p.entity_id IN ('45746')
at the end of INSERT...SELECT
statement that inserts the rule.
array_keys()
to $productIds
before passing it as parameter to Varien_Db_Select::where()
. Later inspection of Magento Community Edition 1.8.1.0 code revealed that Magento core developers fixed this issue in the same way.
Now that you know where's the issue you can upgrade your store, or use any of the more or less Magento ways to override faulty method to fix this until you do upgrade.
That's all for today, I hope you'll find these thoughts and code snippets useful.
Hi
Thanks this post is really very helpfull, many times i face a unique problem in my magento website and nobody able to help me and manytimes difficult to find a right solution in easy way as you describe.
I have some more issues can you help me.
Thanks & Regards
Jagdish
Hi,
I’ve verified comparing that snippet with the 1.9.1.0 one and it’s true.
It was a bug fixed by Magento.
Thank you for this post.. it helped me.
W1
For anyone who has not upgraded to 1.8.1+ this fix is only for websites using flat catalog product option (http://www.magentocommerce.com/wiki/modules_reference/english/mage_adminhtml/system_config/edit/catalog#frontend_field_descriptions)
You will notice that the fix is wrapped with an helper ‘if’ statement that checks for it:
$helper = $this->_factory->getHelper(‘catalog/product_flat’);
if ($helper->isEnabled() && $helper->isBuiltAllStores()) {
If you do not have that option enabled you will not be affected by this.
Can you mention this in the article please?
Cheers,
augsteyer
To anyone trying to get all rules to apply to a newly created / updated product, try the following code:
$product = Mage::getModel('catalog/product') -> load($productId);
$rules = Mage::getModel('catalogrule/rule')->getCollection()->addFieldToFilter('is_active', 1);
foreach ($rules as $rule) {
$rule->applyAllRulesToProduct($product);
}
echo "Applied rules to " . $productId;