Product Stock Alerts (not) working

Recently one of our clients had contacted us and said that ProductAlert functionality doesn’t work any more. After I investigated the situation I saw that last email regarding to Stock Alerts was sent several months ago. In the meantime we’ve upgraded the site to Magento EE and at first I thought that maybe during the upgrade something went wrong. Other thought was that maybe client has modified Transactional Email Template… After reviewing log files I couldn’t find anything related to those emails. System did send other emails though.

During the investigation I saw on forums that other developers have similar issue that weren’t resolved yet. After tracing and looking what might have gone wrong we’ve found… 

Shortly, the issue was in Magento approach to handling Product Stock Alert collection. Our client has more than 40.000 subscribers and more than 30.000 are waiting for status to be changed from “Out of Stock” to “In Stock”. So when Magento tries to load 40.000 records from table product_alert_stock to collection to foreach them because of memory limitation php fails. Probably if you have more than 20.000 records in table product_alert_stock your Product Stock Alert functionality doesn’t work anymore.

The solution was relatively simple. Divide collection by 1.000 and create several pages to iterate through more than 30.000 records, in same way Magento is doing in different places in its system. In our case we had around 35 pages, each with 1.000 records in collection. New functionality is iterating through all of them and for each record where status=0 (unprocessed) system checks for product to see if product is now in stock. If that’s true => set status=1 and send an notification email to customer.

So let’s create our module, Inchoo_ProductAlert, that will enhance Magento’s approach:

1. Create file in app/etc/modules/ Inchoo_ProductAlert.xml with the following content:

<?xml version="1.0"?>
<config>
<modules>
<Inchoo_ProductAlert>
<active>true</active>
<codePool>local</codePool>
</Inchoo_ProductAlert>
</modules>
</config>

2. Create file in app/code/local/Inchoo/ProductAlert/etc/ config.xml with the following content:

<?xml version="1.0"?>
<config>
<modules>
<Inchoo_ProductAlert>
<version>1.0.0.0</version>
</Inchoo_ProductAlert>
</modules>
<global>
<models>
<productalert>
<rewrite>
<observer>Inchoo_ProductAlert_Model_Observer</observer>
</rewrite>
</productalert>
</models>
</global>
</config>

You can see that we’ve rewritten Mage_ProductAlert_Model_Observer class

3. Create file in /www/app/code/local/Inchoo/ProductAlert/Model/ Observer.php with the following content:

<?php
 
/**
* ProductAlert observer
*
* @category Inchoo
* @package Inchoo_ProductAlert
* @author <ivan.galambos@inchoo.net>
*/
class Inchoo_ProductAlert_Model_Observer extends Mage_ProductAlert_Model_Observer
{
/**
* Process stock emails
*
* @param Mage_ProductAlert_Model_Email $email
* @return Mage_ProductAlert_Model_Observer
*/
protected function _processStock(Mage_ProductAlert_Model_Email $email)
{
 
$email->setType('stock');
 
foreach ($this->_getWebsites() as $website) {
 
/* @var $website Mage_Core_Model_Website */
 
if (!$website->getDefaultGroup() || !$website->getDefaultGroup()->getDefaultStore()) {
 
continue;
}
if (!Mage::getStoreConfig(
self::XML_PATH_STOCK_ALLOW,
$website->getDefaultGroup()->getDefaultStore()->getId()
)) {
 
continue;
}
try {
 
$wholeCollection = Mage::getModel('productalert/stock')
->getCollection()
// ->addWebsiteFilter($website->getId())
->addFieldToFilter('website_id', $website->getId())
->addFieldToFilter('status', 0)
;
// $wholeCollection->getSelect()->order('alert_stock_id DESC');
 
/* table: !product_alert_stock!
alert_stock_id: 1
customer_id: 1
product_id: 1
website_id: 1
add_date: 2013-04-26 12:08:30
send_date: 2013-04-26 12:28:16
send_count: 2
status: 1
*/
}
catch (Exception $e) {
Mage::log('error-1-collection $e=' . $e->getMessage(), false, 'product_alert_stock_error.log', true);
$this->_errors[] = $e->getMessage();
return $this;
}
 
$previousCustomer = null;
$email->setWebsite($website);
 
try {
 
$originalCollection = $wholeCollection;
$count = null;
$page = 1;
$lPage = null;
$break = false;
 
while ($break !== true) {
$collection = clone $originalCollection;
$collection->setPageSize(1000);
$collection->setCurPage($page);
$collection->load();
if (is_null($count)) {
$count = $collection->getSize();
$lPage = $collection->getLastPageNumber();
}
if ($lPage == $page) {
$break = true;
}
 
Mage::log('page=' . $page, false, 'check_page_count.log', true);
Mage::log('collection=' . (string)$collection->getSelect(), false, 'check_page_count.log', true);
 
$page ++;
 
foreach ($collection as $alert) {
 
try {
 
if (!$previousCustomer || $previousCustomer->getId() != $alert->getCustomerId()) {
 
$customer = Mage::getModel('customer/customer')->load($alert->getCustomerId());
if ($previousCustomer) {
$email->send();
}
if (!$customer) {
continue;
}
$previousCustomer = $customer;
$email->clean();
$email->setCustomer($customer);
 
}
else {
 
$customer = $previousCustomer;
}
 
$product = Mage::getModel('catalog/product')
->setStoreId($website->getDefaultStore()->getId())
->load($alert->getProductId());
/* @var $product Mage_Catalog_Model_Product */
if (!$product) {
 
continue;
}
 
$product->setCustomerGroupId($customer->getGroupId());
 
if ($product->isSalable()) {
 
$email->addStockProduct($product);
 
$alert->setSendDate(Mage::getModel('core/date')->gmtDate());
$alert->setSendCount($alert->getSendCount() + 1);
$alert->setStatus(1);
$alert->save();
 
}
}
catch (Exception $e) {
Mage::log('error-2-alert $e=' . $e->getMessage(), false, 'product_alert_stock_error.log', true);
$this->_errors[] = $e->getMessage();
}
}
}
 
Mage::log("nn", false, 'check_page_count.log', true);
 
} catch (Exception $e) {
Mage::log('error-3-steps $e=' . $e->getMessage(), false, 'product_alert_stock_error.log', true);
}
 
if ($previousCustomer) {
try {
$email->send();
}
catch (Exception $e) {
$this->_errors[] = $e->getMessage();
}
}
}
 
return $this;
}
 
/**
* Run process send product alerts
*
* @return Inchoo_ProductAlert_Model_Observer
*/
public function process()
{
Mage::log('ProductAlert started @' . now(), false, 'product_alert_workflow.log', true);
 
$email = Mage::getModel('productalert/email');
/* @var $email Mage_ProductAlert_Model_Email */
$this->_processPrice($email);
$this->_processStock($email);
$this->_sendErrorEmail();
 
Mage::log('ProductAlert finished @' . now(), false, 'product_alert_workflow.log', true);
 
return $this;
}
}

You can see that we’ve overwritten 2 methods: process() and _processStock().
In process() method we added Mage::log() for creating start&end time in var/log/product_alert_workflow.log so we can know did script finish with it’s execution. Similarly to debug our enhancement in _processStock() method, we added few more Mage:log() calls so we can track if everything behave in the expected way.

For the end, if you don’t want to wait for your cron to be triggered, you can create a file in Magento root, let’s call it,

inchoo_cron.php:

<?php
/**
* @author <ivan.galambos@inchoo.net>
*/
 
require_once 'app/Mage.php';
Mage::app();
 
try {
Mage::getModel('productalert/observer')->process();
} catch (Exception $e) {
Mage::log('error-0-start $e=' . $e->getMessage() . ' @' . now(), false, 'product_alert_stock_error.log', true);
}

And that’s about it. Feel free after testing your functionality to remove/comment Mage::log() calls. And if you have similar issue with Product Alert Price functionality you can apply the same basic idea and everything should work fine.

3 log files are going to be created if you apply this enhancement without removing Mage::log() calls:

  • product_alert_workflow.log – log start&end time for each call to script
  • check_page_count.log – log #of pages and query that should be run
  • product_alert_stock_error.log – error messages from 0-3 places in code

Note that even Magento EE v. 1.13 has the same problem with big collection in ProductAlert module.

And that’s about it.

Have a nice day!