Problem/Motivation

When an order item's quantity is programmatically set to zero and there also exists a promotion (or an other adjustment) that applies to this order item, a fatal error occurs:

TypeError: Argument 1 passed to Drupal\commerce_price\Calculator::trim() must be of the type string, null given, called in /web/modules/contrib/commerce/modules/price/src/Calculator.php on line 97 in Drupal\commerce_price\Calculator::trim() (regel 236 van /web/modules/contrib/commerce/modules/price/src/Calculator.php)

This didn't happen in Commerce 8.x-2.18, from which I updated.

Additionally, the following PHP warning is logged:

Warning: bcdiv(): Division by zero in Drupal\commerce_price\Calculator::divide() (regel 96 van /web/modules/contrib/commerce/modules/price/src/Calculator.php)

Backtrace:

#0 /web/core/includes/bootstrap.inc(600): _drupal_error_handler_real(2, 'bcdiv(): Divisi...', '/data/sites/web...', 96, Array)
#1 [internal function]: _drupal_error_handler(2, 'bcdiv(): Divisi...', '/data/sites/web...', 96, Array)
#2 /web/modules/contrib/commerce/modules/price/src/Calculator.php(96): bcdiv('0.00', '0.00', 6)
#3 /web/modules/contrib/commerce/modules/price/src/Price.php(169): Drupal\commerce_price\Calculator::divide('0.00', '0.00')
#4 /web/modules/contrib/commerce/modules/order/src/Entity/OrderItem.php(247): Drupal\commerce_price\Price->divide('0.00')
#5 /web/modules/contrib/commerce/modules/promotion/src/Plugin/Commerce/PromotionOffer/OrderItemFixedAmountOff.php(36): Drupal\commerce_order\Entity\OrderItem->getAdjustedUnitPrice(Array)
#6 /web/modules/contrib/commerce/modules/promotion/src/Entity/Promotion.php(571): Drupal\commerce_promotion\Plugin\Commerce\PromotionOffer\OrderItemFixedAmountOff->apply(Object(Drupal\commerce_order\Entity\OrderItem), Object(Drupal\commerce_promotion\Entity\Promotion))
#7 /web/modules/contrib/commerce/modules/promotion/src/PromotionOrderProcessor.php(55): Drupal\commerce_promotion\Entity\Promotion->apply(Object(Drupal\commerce_order\Entity\Order))
#8 /web/modules/contrib/commerce/modules/order/src/OrderRefresh.php(161): Drupal\commerce_promotion\PromotionOrderProcessor->process(Object(Drupal\commerce_order\Entity\Order))
#9 /web/modules/contrib/commerce/modules/order/src/OrderStorage.php(131): Drupal\commerce_order\OrderRefresh->refresh(Object(Drupal\commerce_order\Entity\Order))
#10 /web/modules/contrib/commerce/modules/order/src/OrderStorage.php(108): Drupal\commerce_order\OrderStorage->doOrderPreSave(Object(Drupal\commerce_order\Entity\Order))
#11 /web/core/lib/Drupal/Core/Entity/EntityStorageBase.php(500): Drupal\commerce_order\OrderStorage->invokeHook('presave', Object(Drupal\commerce_order\Entity\Order))
#12 /web/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php(700): Drupal\Core\Entity\EntityStorageBase->doPreSave(Object(Drupal\commerce_order\Entity\Order))
#13 /web/core/lib/Drupal/Core/Entity/EntityStorageBase.php(454): Drupal\Core\Entity\ContentEntityStorageBase->doPreSave(Object(Drupal\commerce_order\Entity\Order))
#14 /web/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php(837): Drupal\Core\Entity\EntityStorageBase->save(Object(Drupal\commerce_order\Entity\Order))
#15 /web/core/lib/Drupal/Core/Entity/EntityBase.php(395): Drupal\Core\Entity\Sql\SqlContentEntityStorage->save(Object(Drupal\commerce_order\Entity\Order))
#16 /web/modules/contrib/commerce/modules/order/src/OrderStorage.php(164): Drupal\Core\Entity\EntityBase->save()
#17 /web/core/lib/Drupal/Core/Entity/EntityStorageBase.php(307): Drupal\commerce_order\OrderStorage->postLoad(Array)
#18 /web/modules/contrib/commerce/modules/cart/src/CartProvider.php(225): Drupal\Core\Entity\EntityStorageBase->loadMultiple(Array)
#19 /web/modules/contrib/commerce/modules/cart/src/CartProvider.php(177): Drupal\commerce_cart\CartProvider->loadCartData(Object(Drupal\Core\Session\AccountProxy))
#20 /web/modules/contrib/commerce/modules/cart/src/CartProvider.php(165): Drupal\commerce_cart\CartProvider->getCartIds(NULL)
#21 /web/modules/contrib/commerce/modules/cart/src/Plugin/Block/CartBlock.php(221): Drupal\commerce_cart\CartProvider->getCarts()
#22 /web/core/modules/block/src/BlockViewBuilder.php(47): Drupal\commerce_cart\Plugin\Block\CartBlock->getCacheTags()
#23 /web/core/modules/block/src/BlockViewBuilder.php(32): Drupal\block\BlockViewBuilder->viewMultiple(Array, 'full', NULL)
#24 /web/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php(151): Drupal\block\BlockViewBuilder->view(Object(Drupal\block\Entity\Block))
#25 /web/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php(259): Drupal\block\Plugin\DisplayVariant\BlockPageVariant->build()
#26 /web/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php(117): Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object(Symfony\Component\HttpFoundation\Request), Object(Drupal\Core\Routing\CurrentRouteMatch))
#27 /web/core/lib/Drupal/Core/EventSubscriber/MainContentViewSubscriber.php(90): Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object(Symfony\Component\HttpFoundation\Request), Object(Drupal\Core\Routing\CurrentRouteMatch))
#28 [internal function]: Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object(Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent), 'kernel.view', Object(Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher))
#29 /web/core/lib/Drupal/Component/EventDispatcher/ContainerAwareEventDispatcher.php(111): call_user_func(Array, Object(Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent), 'kernel.view', Object(Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher))
#30 /vendor/symfony/http-kernel/HttpKernel.php(156): Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch('kernel.view', Object(Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent))
#31 /vendor/symfony/http-kernel/HttpKernel.php(68): Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object(Symfony\Component\HttpFoundation\Request), 1)
#32 /web/core/lib/Drupal/Core/StackMiddleware/Session.php(57): Symfony\Component\HttpKernel\HttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#33 /web/core/lib/Drupal/Core/StackMiddleware/KernelPreHandle.php(47): Drupal\Core\StackMiddleware\Session->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#34 /web/core/modules/page_cache/src/StackMiddleware/PageCache.php(106): Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#35 /web/core/modules/page_cache/src/StackMiddleware/PageCache.php(85): Drupal\page_cache\StackMiddleware\PageCache->pass(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#36 /web/core/modules/ban/src/BanMiddleware.php(50): Drupal\page_cache\StackMiddleware\PageCache->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#37 /web/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php(47): Drupal\ban\BanMiddleware->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#38 /web/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php(52): Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#39 /vendor/stack/builder/src/Stack/StackedHttpKernel.php(23): Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#40 /web/core/lib/Drupal/Core/DrupalKernel.php(708): Stack\StackedHttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#41 /web/index.php(19): Drupal\Core\DrupalKernel->handle(Object(Symfony\Component\HttpFoundation\Request))
#42 {main}

Background / Use case for setting a order item's quantity to zero

I've setup a store where customers can register for events. Providing the registrants happens during checkout. At the checkout step 'Event registration', customers can add, edit and remove registrants for the event. When no registrants are added for an event yet, the order item's quantity should be zero at that point, but not removed from the cart. I'm using my sandbox module Commerce RNG for that.

Proposed resolution

Do not divide the order item's total price when the order item's quantity is zero.

Remaining tasks

User interface changes

API changes

Data model changes

Possibly related issues:

#2930979: don't show the 'added to your cart' message if the item quantity is unchanged or 0; make CartEntityAddEvent aware of the original order item
#3115150: Submitting add to cart form with empty quantity value results in an exception
#3030247: When last item is removed from cart, shippings and coupons are not removed

Patch will follow.

Issue fork commerce-3156089

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

MegaChriz created an issue. See original summary.

megachriz’s picture

Assigned: megachriz » Unassigned
Status: Active » Needs review
StatusFileSize
new3.4 KB
new4.25 KB

Here is a patch with tests. The test only patch should fail.

I started with writing a test for Commerce Promotion as my first thoughts were that the error might be there until I it seemed more logical to me to fix it in OrderItem - and found out that the error could occur with other adjustment types as well. I left in the test for Commerce Promotion because that one passes on Commerce 8.x-2.18, while the test for OrderItem doesn't.

The last submitted patch, 2: commerce-order-item-qty-zero-3156089-tests-only.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

anybody’s picture

Version: 8.x-2.x-dev » 8.x-2.40
Component: Order » Price
Priority: Normal » Major

Just ran into a similar issue in latest 2.40! This shouldn't happen.
We're going to have a closer look after the holidays!

DivisionByZeroError: Division by zero in bcdiv() (Zeile 96 in /web/modules/contrib/commerce/modules/price/src/Calculator.php).

#0 /web/modules/contrib/commerce/modules/price/src/Calculator.php(96): bcdiv()
#1 /web/modules/contrib/commerce/modules/price/src/Price.php(169): Drupal\commerce_price\Calculator::divide()
#2 /web/modules/contrib/commerce/modules/order/src/Entity/OrderItem.php(256): Drupal\commerce_price\Price->divide()
#3 /web/modules/contrib/google_tag/src/Plugin/GoogleTag/Event/Commerce/AddToCartEvent.php(38): Drupal\commerce_order\Entity\OrderItem->getAdjustedUnitPrice()
#4 /web/modules/contrib/google_tag/google_tag.module(147): Drupal\google_tag\Plugin\GoogleTag\Event\Commerce\AddToCartEvent->getData()
#5 /web/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php(311): google_tag_page_attachments()
#6 /web/core/lib/Drupal/Core/Extension/ModuleHandler.php(395): Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}()
#7 /web/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php(308): Drupal\Core\Extension\ModuleHandler->invokeAllWith()
#8 /web/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php(285): Drupal\Core\Render\MainContent\HtmlRenderer->invokePageAttachmentHooks()
#9 /web/core/lib/Drupal/Core/Render/Renderer.php(638): Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}()
#10 /web/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php(284): Drupal\Core\Render\Renderer->executeInRenderContext()
#11 /web/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php(128): Drupal\Core\Render\MainContent\HtmlRenderer->prepare()
#12 /web/core/lib/Drupal/Core/EventSubscriber/MainContentViewSubscriber.php(90): Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse()
#13 [internal function]: Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray()
#14 /web/core/lib/Drupal/Component/EventDispatcher/ContainerAwareEventDispatcher.php(111): call_user_func()
#15 /vendor/symfony/http-kernel/HttpKernel.php(186): Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch()
#16 /vendor/symfony/http-kernel/HttpKernel.php(76): Symfony\Component\HttpKernel\HttpKernel->handleRaw()
#17 /web/core/lib/Drupal/Core/StackMiddleware/Session.php(53): Symfony\Component\HttpKernel\HttpKernel->handle()
#18 /web/core/lib/Drupal/Core/StackMiddleware/KernelPreHandle.php(48): Drupal\Core\StackMiddleware\Session->handle()
#19 /web/core/lib/Drupal/Core/StackMiddleware/ContentLength.php(28): Drupal\Core\StackMiddleware\KernelPreHandle->handle()
#20 /web/core/modules/big_pipe/src/StackMiddleware/ContentLength.php(32): Drupal\Core\StackMiddleware\ContentLength->handle()
#21 /web/core/modules/page_cache/src/StackMiddleware/PageCache.php(116): Drupal\big_pipe\StackMiddleware\ContentLength->handle()
#22 /web/core/modules/page_cache/src/StackMiddleware/PageCache.php(90): Drupal\page_cache\StackMiddleware\PageCache->pass()
#23 /web/core/modules/ban/src/BanMiddleware.php(50): Drupal\page_cache\StackMiddleware\PageCache->handle()
#24 /web/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php(48): Drupal\ban\BanMiddleware->handle()
#25 /web/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php(51): Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle()
#26 /web/core/lib/Drupal/Core/StackMiddleware/AjaxPageState.php(36): Drupal\Core\StackMiddleware\NegotiationMiddleware->handle()
#27 /web/modules/contrib/remove_http_headers/src/StackMiddleware/RemoveHttpHeadersMiddleware.php(49): Drupal\Core\StackMiddleware\AjaxPageState->handle()
#28 /web/core/lib/Drupal/Core/StackMiddleware/StackedHttpKernel.php(51): Drupal\remove_http_headers\StackMiddleware\RemoveHttpHeadersMiddleware->handle()
#29 /web/core/lib/Drupal/Core/DrupalKernel.php(741): Drupal\Core\StackMiddleware\StackedHttpKernel->handle()
#30 /web/index.php(19): Drupal\Core\DrupalKernel->handle()
#31 {main}
grevil’s picture

The issue from @Anybody and I would be solved in #3495551: Error: Call to a member function getEntityTypeId() on null, when order item quantity is "0". We wrongly allowed to set a quantity of "0", which resulted in the error.

jsacksick’s picture

@grevil: But the issue linked is a Commerce API issue, not a Commerce one.

grevil’s picture

@jsacksick Indeed! But we ended up in commerce, since we unintentionally allowed a quantity of "0". The original entry point was "commerce_api".

anybody’s picture

@jsacksick if we want to proceed here, this probably needs to be rebased against 3.x as MR. We can do that, if you like the approach.

Test-only should show if the issue still exists?

jsacksick’s picture

Version: 8.x-2.40 » 3.x-dev

Looking at the patch, not sure in what scenario the order item has a total price, but no unit price? Because it's getting the currency code from its total.

jsacksick’s picture

@anybody: Opened an MR, and adjusted the changes slightly. Are you experiencing this?

anybody’s picture

@jsacksick thank you very much!

MR looks good, let's see if the tests succeed.
We ran into this in the past, as written in #5 in an edge-case by accident. This case doesn't exist any more. But I think this should be handled more gracefully, which should be the case now.

Hopefully @megachriz can do the final review?

anybody’s picture

Priority: Major » Normal

  • jsacksick committed dd7bee26 on 3.x
    fix: #3156089 Price calculation fails with a fatal error on zero...
jsacksick’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.