Product Stock Alerts (not) working

subscribe

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("\n\n", 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!

12
Top

Care to rate this post?

Author

Ivan Galambos

Backend Developer

Ivan worked at Inchoo from February 2011 to November 2013 as a Backend Developer.

Other posts from this author

Discussion 12 Comments

Add Comment
  1. Robert Readman

    Thanks for the post with a fix.
    Since you know the code, why not raise a bug tracking issue for magento-1.8.0.0-alpha1.
    http://www.magentocommerce.com/bug-tracking/report/

    You could list the core file(s) that need changing with the new code, then hopefully this will be fixed in magento CE 1.8 and EE 1.14

    In magneto 1.8.0.0-alpha1I’ve noticed they changed the php_value memory_limit from 128M to 512M in the .htaccess.sample file, however stays as a default of 256M in the .htaccess.

    What was the memory limit of your client?

  2. Hi Robert,

    I just posted the report:
    “Thank you for submitting the issue. Your report was sent to our development team.”

    Memory limit was 128MB / 800MB.

  3. Chinthaka

    Hi Ivan,

    Is it possible to send this alert when the user updates the Product stock without using a cron job?

  4. @Chinthaka

    For now no on easy way, because you’ll have to iterate through all collection – it can lasts for more than 20 minutes, depending on how many customers are subscribed – administrators don’t want to wait so long :). Anyway, nice idea and I’ll create extension for you so you can do that also – maybe we’ll left a note how many notification emails system sent to subscribers of specific product.

    Nice comment!

    Thank you,
    Ivan

  5. Sammy

    Thought I finally found the solution to my problems, but when trying to run the inchoo_cron.php, I get this:

    HTTP Error 500 (Internal Server Error): An unexpected condition was encountered while the server was attempting to fulfill the request.

    Copy pasted everything 100%, even thought that the php files needed this at the end “?>” and tried that, too. Nothing :(.

    Can’t get my product alerts — driving me nuts. Really appreciate the resource!

  6. Hi Sammy,

    Wysiwyg from WordPress has maybe modified some HTML entities in time of posting this solution. Please modify them if that’s the case. Please check all HTML entities in the code provided above and modify them if needed. Everything should work fine if your characters look same as here on screen.

    Additionally please check your file permissions and then it should work fine. Which Magento version are you using?

    Check your error logs and try to provide more info if nothing else helps.

    Cheers

  7. Ron

    Thanks a lot,
    Worked nice and smooth.

    2 comments/Q’s:

    1. I added a cron job to run the inchoo_cron.php – works great.
    2. (Q) For some reason the mail template is not the new one I made- it uses the default. Any Ideas why?

    Again – Thank you inchoo team!

  8. Hi Ron,

    I believe it’s because of cache… try to clear Magento’s cache… I believe after that everything should work fine…

    gl

  9. Ron

    Thanks, but I tried t before hand, and again:
    Flushed magento cache” and “cache storage”.

    Same issue. I guess I’ll have to change the template itself…

  10. ibrahim

    Hi Ivan ;

    it gives that error ? any idea

    Fatal error: Class ‘Â Â Â Â Mage’ not found in /httpdocs/inchoo_cron.php on line 8

  11. Hi Ibrahim,

    I believe that you have some error in your file. Try to create new .php file and try to type all required code there. I believe that you have some encoding issue…

    GL

  12. Jay Essu

    Hello,

    nice improvement there!

    I have a question: in my installation EE 1.13 the stock alert subscribers are always stored on

    stock_notify_unregistered_users

    table, even when the user is logged in. The fun thing is that the alert emails are sent correctly but with an empty body (alertGrid). In my local, however, the body is correct (same template as my production), but it doesn’t sends the emails if the subscribers are not in the

    product_alert_stock

    table.

    Any idea why this could happened? I’m kind of confused here.

    Thanks!

Add Your Comment

Please wrap all source codes with [code][/code] tags.
Top