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.
| Comment | File | Size | Author |
|---|---|---|---|
| #2 | commerce-order-item-qty-zero-3156089.patch | 4.25 KB | megachriz |
| #2 | commerce-order-item-qty-zero-3156089-tests-only.patch | 3.4 KB | megachriz |
Issue fork commerce-3156089
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
Comment #2
megachrizHere 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.
Comment #4
anybodyJust ran into a similar issue in latest 2.40! This shouldn't happen.
We're going to have a closer look after the holidays!
Comment #5
grevil commentedThe 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.
Comment #6
jsacksick commented@grevil: But the issue linked is a Commerce API issue, not a Commerce one.
Comment #7
grevil commented@jsacksick Indeed! But we ended up in commerce, since we unintentionally allowed a quantity of "0". The original entry point was "commerce_api".
Comment #8
anybody@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?
Comment #9
jsacksick commentedLooking 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.
Comment #11
jsacksick commented@anybody: Opened an MR, and adjusted the changes slightly. Are you experiencing this?
Comment #12
anybody@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?
Comment #13
anybodyComment #15
jsacksick commented