EE 1.14.2 / CE 1.9.2: Quote items not merged correctly on login (duplicate products in cart)

Nice wrap up of the bug above, Fabian!

For any further users who will come accross this bug, there is already a patch from Magento for this.

As an Enterprise customer, you can request/download PATCH_SUPEE-6190_EE_1.14.2.0_v1.sh to fix this.

Update 24.02.2016: This was also addressed in the latest SUPEE-7405 v 1.1 patch. According to Fabian on Twitter (see this and following tweets) there is a chance it's still not resolved completely. Please test it yourself also.

As for EE 1.14.2.0 the solution is:

diff --git a/app/code/core/Mage/Sales/Model/Quote/Item.php b/app/code/core/Mage/Sales/Model/Quote/Item.php
index 3554faa..d759249 100644
--- a/app/code/core/Mage/Sales/Model/Quote/Item.php
+++ b/app/code/core/Mage/Sales/Model/Quote/Item.php
@@ -502,8 +502,8 @@ class Mage_Sales_Model_Quote_Item extends Mage_Sales_Model_Quote_Item_Abstract
                         $itemOptionValue = $_itemOptionValue;
                         $optionValue = $_optionValue;
                         // looks like it does not break bundle selection qty
-                        unset($itemOptionValue['qty'], $itemOptionValue['uenc']);
-                        unset($optionValue['qty'], $optionValue['uenc']);
+                        unset($itemOptionValue['qty'], $itemOptionValue['uenc'], $itemOptionValue['form_key']);
+                        unset($optionValue['qty'], $optionValue['uenc'], $optionValue['form_key']);
                     }
                 }

Note: Usually I would not post EE code here, but as the problem/files are the same as in CE and do not affect a EE-only feature, I hope it's ok.


It turned out that this is a bug in Mage_Sales_Model_Quote_Item::compare() that was introduced in Magento CE 1.9.2 / EE 1.14.2. The method is used to compare items to decide if they are the same product and can be merged (during login and when adding products to the cart).

When comparing all custom options, it should skip the options that are not represantative (_notRepresentOptions), namely the info_buyRequest option.

In previous Magento versions, it looked like this:

foreach ($this->getOptions() as $option) {
    if (in_array($option->getCode(), $this->_notRepresentOptions)) {
        continue;
    }

and worked correctly. Now it looks like this:

foreach ($this->getOptions() as $option) {
    if (in_array($option->getCode(), $this->_notRepresentOptions)
        && !$item->getProduct()->hasCustomOptions()
    ) {
        continue;
    }

and the additional check for hasCustomOptions() causes the described bug. Why? It looks like the check has been added to always keep products with custom options separate. I don't think it makes sense, at least not in the way it is implemented, but there will be some reason for it that I am not aware of.

However, $item->getProduct()->hasCustomOptions() always returns true for quote items!

This is the method:

public function hasCustomOptions()
{
    if (count($this->_customOptions)) {
        return true;
    } else {
        return false;
    }
}

But $this->_customOptions also contains the info_buyRequest option from the quote item.

For an unobstrusive solution, I tried to remove the info_buyRequest option from all products in an observer on sales_quote_merge_before, with no success.

The reason lies in Mage_Sales_Model_Quote_Item_Abstract::getProduct() where the option is copied again from the quote item itself:

public function getProduct()
{
    $product = $this->_getData('product');

    [...]

    if (is_array($this->_optionsByCode)) {
        $product->setCustomOptions($this->_optionsByCode);
    }
    return $product;
}

Solution

I created a rewrite for Mage_Sales_Model_Quote_Item with an override for getProduct() to not include the info_buyRequest option at this point:

public function getProduct() { $product = parent::getProduct(); $options = $product->getCustomOptions(); if (isset($options['info_buyRequest'])) { unset($options['info_buyRequest']); $product->setCustomOptions($options); } return $product; }

This caused trouble with bundle products, the alternative below or the official patch as described by @AnnaVölkl is a better solution

Alternative

You could also remove the offending && !$item->getProduct()->hasCustomOptions() in the compare() method if you are rewriting the item model anyways. I don't know what problem it tried to solve, but it created more...

Update Jan 29 2016

I reported this to Magento and got the response that they could not reproduce the issue, so the patch will not make it into the community edition (Submission APPSEC-1321).

This means, if you have the problem, you need to apply the enterprise patch SUPEE-6190 after each update or use a class rewrite instead.