This issue is regarding Commerce Stock Reserve causing an incorrect calculation of taxes by Commerce. To recreate the issue:

  1. Create a product with a run-time tax calculation. Assume the nominal price is 10.00 and the tax is 20%.
  2. Add the product to the cart. The amount shown to the user is 12.00, as expected.
  3. Change the number of line-items in the cart from 1 to 2. The expected final amount is 24.00. The one shown is 28.80. I.e, the tax has been applied twice.
  4. If one queries the record of the product from the database, it will now contain the price of 12.00 instead of 10.00. I.e., the database has been overridden.

Here is how it's all related to Commerce Stock Reserve: it is the function commerce_stock_reserve() that overrides the products database, since it alters the stock levels of the cached record of the product (which has been already modified by Commerce to account for the taxes), instead of its original version in the database, containing the original price of 10.00.

The patch attached is a rough fix for the issue. I have no doubts, there are more elegant solutions. Please, help.

Thanks in advance!

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Roman.UCLU created an issue. See original summary.

pjcdawkins’s picture

Version: 7.x-1.3 » 7.x-1.x-dev

I just noticed this issue. If I understand it correctly, that sounds like a Commerce core bug to me. Just changing an unrelated field on a product (the stock field) shouldn't mess up its price. This module doesn't touch prices, and I don't think it should.

I wonder, are you using a Redis (or non-SQL) cache for the cache_field bin?

Roman.UCLU’s picture

I wonder, are you using a Redis (or non-SQL) cache for the cache_field bin?

Thank you for your reply. No, we don't use any of the above.

I did some further debugging. It happens that the issue is significantly more complicated than I originally thought, but I still believe that it's Commerce Stock Reserve that needs to be patched. The bug results from incorrect interactions among four different modules: Commerce Stock Reserve, Commerce Product Attributes, Commerce Product Pricing, Commerce Tax. The last two are submodules of Commerce. The following happens:

  1. A view is generated to display the line items in the cart of the user.
  2. Commerce Product Attributes is a module that allows displaying product attributes within the aforementioned view. To accomplish that, Commerce Product Attributes adds the products corresponding to the line items in the cart as fields to the view.
  3. Commerce Product Pricing is a module that formats prices of products in views that contain them. It defines hooks, such as _field_formatter_prepare_view() that would allow it to act upon a product field being added to the display. Then it calls the modules necessary (e.g. Commerce Tax) to do the formatting for the price of this product.
  4. In this case, Commerce Product Pricing reacts to the product field being added by Commerce Product Attributes. It treats it just like any other product field: it attempts to format its price with Commerce Tax.
  5. Commerce Product Pricing, of course, should only modify these prices for the purposes of displaying only. These changes should never end up in the database. This is why it creates an additional field for every product, original, where it stores the old price of the product to be restored when the view is fully formed.
  6. What Commerce Product Pricing doesn't know is that there is another module that also modifies these products: Commerce Stock Reserve. Only as opposed to Commerce Product Pricing, Commerce Stock Reserve makes its modifications permanent by saving them in the database.
  7. In the end, Commerce Stock Reserve saves both the changes it has made (the stock levels) and the temporary changes that Commerce Product Pricing has made, which were meant to be removed a little later and never show up in the database.

One might argue that the reason behind this bug is the bad architecture of Commerce Product Pricing, since this module assumes that nothing would be working with products at the same time when it does. However, rebuilding the architecture of a submodule of Commerce would be too complicated.

It was suggested that the best course of action is to make Commerce Stock Reserve independent of anything that can be messing up with products while it is doing its job, regardless of it being Commerce Product Pricing or anything else that a particular Drupal configuration might contain. This would be possible, if Commerce Stock Reserve retrieved its products directly from the database, instead of using their versions in the RAM, as it does now.

The patch is attached.

pjcdawkins’s picture

Thanks for the analysis!

I'm not confident that this will fix the issue, because I haven't had the time to reproduce it myself, but if you can test it... I'm happy.

I've attached a modification of your patch - it's functionally the same - with a few small tweaks:

  • added a comment to explain why it's being done this way
  • all caps for TRUE (sadly this is a Drupal-specific thing)
  • used ->getIdentifier() instead of ->id->raw() because I think it's clearer

By the way, you can set issues to Needs review when you upload a patch (and Needs work when an existing patch needs changes).

Status: Needs review » Needs work

The last submitted patch, 4: commerce_stock_reserve-2732251-4.patch, failed testing.

pjcdawkins’s picture

Oh hang on, entity_load() won't cut it, it returns an array. New patch attached.

Speaking of which, the helper functions entity_load_single() and commerce_product_load() could really do with a $reset argument.

Status: Needs review » Needs work

The last submitted patch, 6: commerce_stock_reserve-2732251-5.patch, failed testing.

Roman.UCLU’s picture

Once again, thank you very much for your input! Indeed, the patch I submitted wouldn't be functional. I guess, I failed to test it properly.

I have tested the latest patch on my configuration and can confirm that it does solve this issue!

Roman.UCLU’s picture

Status: Needs work » Fixed
pjcdawkins’s picture

Status: Fixed » Reviewed & tested by the community
  • "Reviewed and tested by the community" - when you have reviewed + tested a patch
  • "Fixed" - when the change has actually been committed to the project

(see https://www.drupal.org/node/156119)

  • pjcdawkins committed c62b36a on 7.x-1.x
    Issue #2732251 by Roman.UCLU, pjcdawkins: Incorrect calculation of tax
    
pjcdawkins’s picture

Status: Reviewed & tested by the community » Fixed

... now committed (to 7.x-1.x-dev)

The last submitted patch, commerce_stock_reserve-incorrect_tax.patch, failed testing.

The last submitted patch, commerce_stock_reserve-incorrect_tax.patch, failed testing.

The last submitted patch, 3: commerce_stock_reserve-incorrect_tax-7.patch, failed testing.

The last submitted patch, 3: commerce_stock_reserve-incorrect_tax-7.patch, failed testing.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

Roman.UCLU’s picture

I am reopening this issue due to a knock-on effect caused by the latest patch. Namely, the automated testing of the module returns failures when ran.

Below is the code causing the issue from testReserve() of commerce_stock_reserve_basic.test:

    $product = $this->createDummyProduct();
...
    // Create an order containing the product.
    $quantity = rand(1, 10);
    $order = $this->createDummyOrder(1, array(
      $product->product_id => $quantity,
    ));
...
    $actual_stock = commerce_stock_reserve_get_product_stock($product, TRUE);

Note that $product is not passed or returned by createDummyOrder(). Nonetheless, the function assumes that after createDummyOrder() is called, $product will pick up the change automatically. In other words, the function expects createDummyOrder() to find $product in the memory and modify it, even though createDummyOrder() does not even know that $product as well as the code using it exist. I do not believe that this approach is correct.

The test used to work before, since, indeed, the Entity Caching mechanism of Drupal forces createDummyOrder() to use our $product, which I consider a lucky coincidence. In general, several rules are invoked within createDummyOrder() which trigger several completely unrelated modules. It is somewhat naive to expect all of them to use caching.

In our case, the test fails with the patch, since in our patch we call entity_load(), which clears the entity cache for the entity being loaded. Hence, the alterations Commerce Stock Reserve does to the stock value are no longer stored in $product, the test cannot see them and returns a failure.

In the patch attached I use commerce_product_load() to locate the right place where the changes are stored. Once picked up by the system, the test is back to being successful.

Status: Needs review » Needs work

The last submitted patch, 18: commerce_stock_reserve-2732251-13.patch, failed testing.

The last submitted patch, 18: commerce_stock_reserve-2732251-13.patch, failed testing.

Roman.UCLU’s picture

Status: Needs work » Needs review

pjcdawkins’s picture

Status: Needs review » Fixed

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.