I have a site where all of the items for sale are one-off. My problem is that although the module does a good job of not allowing me to add a second item of one type to my shopping cart, if I open a second browser, I can add that item to my cart. The implication here is that 2 people can order an item when only one item is available.

Have I missed something? Is there a way to decrement the available quantity to 0 as soon as the item is placed in the cart?

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

d2d created an issue. See original summary.

guy_schneerson’s picture

Only one of those should be able to complete checkout. It is not uncommon for people to add an item in their basket and never come back to the site and you do not want that item to become unavailable as a result.

d2d’s picture

Thank you. That clears things up. I will try to convince the site owner that everything is OK. She may not be happy, but as you point out, the alternative is worse.

krystalcode made their first commit to this issue’s fork.

krystalcode’s picture

I had a similar requirement recently.

I have created an MR that does the following:
- Configuration option to determine whether stock should be reserved upon order placement or when adding products to cart.
- Two new transaction types: Reservation and Release.
- When products are added to the cart, or order items are updated/deleted, stock is reserved or released accordingly.
- When an abandoned cart is deleted (e.g. through the delete abandoned carts feature) stock is released.
- When the order is placed stock reservations are converted to stock sales.

No tests written yet.

It does not address the concern that the stock may be reserved and then users abandon carts; the stock will stay reserved till the abandoned cart is deleted. But it's a starting point, I think we can find a solution about that as well.

What if the stock reservation is cancelled after the user session ends? Or after a configurable amount of time. We may need a 2nd quantity field on the order item so that desired quantity is not lost in that case.

jason ruyle’s picture

StatusFileSize
new36.35 KB

I generated a patch from your branch 3169030-decrement-available-quantity

I did run into issues on the checkout process, after I submit from "Pay and complete", I get this error:

The website encountered an unexpected error. Please try again later.
Error: Call to undefined method Drupal\state_machine\Event\WorkflowTransitionEvent::getOrderItems() in Drupal\commerce_stock\EventSubscriber\CartEventSubscriber->onCartOrderPlace() (line 133 of modules/contrib/commerce_stock/src/EventSubscriber/CartEventSubscriber.php).
Drupal\commerce_stock\EventSubscriber\CartEventSubscriber->onCartOrderPlace(Object, 'commerce_order.place.post_transition', Object)
call_user_func(Array, Object, 'commerce_order.place.post_transition', Object) (Line: 111)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch('commerce_order.place.post_transition', Object) (Line: 353)
Drupal\state_machine\Plugin\Field\FieldType\StateItem->dispatchTransitionEvent('post_transition') (Line: 323)
Drupal\state_machine\Plugin\Field\FieldType\StateItem->postSave(1)
call_user_func_array(Array, Array) (Line: 233)
Drupal\Core\Field\FieldItemList->delegateMethod('postSave', 1) (Line: 198)
Drupal\Core\Field\FieldItemList->postSave(1)
call_user_func_array(Array, Array) (Line: 881)
Drupal\Core\Entity\ContentEntityStorageBase->invokeFieldMethod('postSave', Object, 1) (Line: 913)
Drupal\Core\Entity\ContentEntityStorageBase->invokeFieldPostSave(Object, 1) (Line: 839)
Drupal\Core\Entity\ContentEntityStorageBase->invokeHook('update', Object) (Line: 100)
Drupal\commerce\CommerceContentEntityStorage->invokeHook('update', Object) (Line: 111)
Drupal\commerce_order\OrderStorage->invokeHook('update', Object) (Line: 535)
Drupal\Core\Entity\EntityStorageBase->doPostSave(Object, 1) (Line: 728)
Drupal\Core\Entity\ContentEntityStorageBase->doPostSave(Object, 1) (Line: 460)
Drupal\Core\Entity\EntityStorageBase->save(Object) (Line: 837)
Drupal\Core\Entity\Sql\SqlContentEntityStorage->save(Object) (Line: 395)
Drupal\Core\Entity\EntityBase->save() (Line: 174)
Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowBase->redirectToStep('complete') (Line: 180)
Drupal\commerce_payment\Plugin\Commerce\CheckoutPane\PaymentProcess->buildPaneForm(Array, Object, Array) (Line: 559)
Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowWithPanesBase->buildForm(Array, Object, 'payment')
call_user_func_array(Array, Array) (Line: 532)
Drupal\Core\Form\FormBuilder->retrieveForm('commerce_checkout_flow_multistep_default', Object) (Line: 278)
Drupal\Core\Form\FormBuilder->buildForm(Object, Object) (Line: 219)
Drupal\Core\Form\FormBuilder->getForm(Object, 'payment') (Line: 94)
Drupal\commerce_checkout\Controller\CheckoutController->formPage(Object)
call_user_func_array(Array, Array) (Line: 123)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 573)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 124)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext(Array, Array) (Line: 97)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 151)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 68)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 57)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 106)
Drupal\page_cache\StackMiddleware\PageCache->pass(Object, 1, 1) (Line: 85)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 52)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 708)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)

In terms of removing items from the cart, couldn't the store owners set the time for abandoned carts to something a lot lower (like 30 minutes). Then on cronjobs, those carts would be set to abandoned and at that stage, the stock would be removed?

krystalcode’s picture

Thanks for testing.

Line 133:
foreach ($event->getOrderItems() as $item) { should be
foreach ($order->getOrderItems() as $item) {

jason ruyle’s picture

I still got the same error with changing just event to order.

I did a quick test with the following on line 133.

foreach ($event->getOrderItems() as $item) {

to:

foreach ($order->getItems() as $item) {

And there was no error.

I checked the commerce_stock_transaction table and the transaction id is being logged properly. Changes from 10 (STOCK_RELEASE) to 4 (STOCK_SALE).

jason ruyle’s picture

StatusFileSize
new36.34 KB

Here is the patch from #9.

damienmckenna’s picture

Status: Active » Needs review

Should it instead double-check the inventory as part of the checkout process? On one site I'm involved in they regularly have people add items to their cart and leave them there for several days; this change could potentially stop would prevent others from completing their transaction.

While I do appreciate the idea of a customer walking around in a store with an item in their cart they haven't bought yet, in this analogy the customer wouldn't continue walking around the store for two weeks deciding whether they wanted to buy it.

Many sites that have tight inventory controls will have a timer that requires a visitor to complete the checkout within a certain amount of time, otherwise the item is made available for others. This is common with event ticket sales, e.g. concerts, etc.

damienmckenna’s picture

krystalcode’s picture

Configuration option to determine whether stock should be reserved upon order placement or when adding products to cart.

This is optional. If that is not the requirement for your site, do not configure it to reserve the item when it is added to the cart.

gidarai’s picture

StatusFileSize
new44.79 KB

Rerolled the patch from #10 and added numerous fixes.

- The 'stock reservation upon adding to cart' feature is now fully optional
- Patch now works with the latest version
- Improved stock form functionality
- Added support for stock enforcement

My colleague (@arantxio.gottmers) and i have been working on this as we need it for our site and thought it would be beneficial to share it here as well.

arantxio’s picture

StatusFileSize
new44.81 KB
new675 bytes

I've noticed that it missed a import, so here is the adjusted patch.

gidarai’s picture

Version: 8.x-1.x-dev » 3.x-dev
Status: Needs review » Reviewed & tested by the community

This patch also works for version 3.x, so i'll up the target version. Can this be committed if there's nothing holding it back?

We have been using this patch for our production site for about a few months now without any issues.

guy_schneerson’s picture

Amazing work on this.
I am happy to add this feature, although it is not a complete solution as stock does not support the expression of the cart and does not have a dependency on a module that can.
But as it's optional, I think we can add it (I may reword it a bit but that's not a blocking issue).
I will, however, only commit new features to the 3.x branch.
As this is a significant update, I will need to review it properly before committing. So far all looks good and works as expected.

starlight-sparkle’s picture

I've observed what seems like a possible issue with the design of patch #14/#15.
The implementation of stock enforcement in the patch doesn't seem to account for offsite payment flows, which need to lock the cart and then render the checkout payment process pane in its own dedicated checkout step. Attempting to make offsite payment with this patch results in customers being returned to the /cart page and being prevented from proceeding with checkout.

The patched commerce_stock_enforcement_is_order_in_stock() function assumes that the current user's reserved quantity can be reliably calculated via commerce_stock_enforcement_get_ordered_quantity(), which in turn retrieves cart quantities by calling CartProvider::getCarts().
However, CartProvider::getCarts() only includes unlocked carts, so when you proceed to the payment checkout step using an offsite payment method, the cart you're trying to make payment for is locked, and stops being counted in the "reserved quantity", thus you get redirected back to cart and can never complete payment.

commerce_stock_enforcement_is_order_in_stock() probably needs to calculate reserved quantity by some other way.

starlight-sparkle changed the visibility of the branch master to hidden.

starlight-sparkle changed the visibility of the branch 3.x to hidden.

starlight-sparkle’s picture

Status: Reviewed & tested by the community » Needs review

Added a query condition to LocalStockChecker::getLocationStockTransactionLatest() to exclude "reservation" transactions if called from LocalStockChecker::getTotalStockLevel() and not if called from LocalStockChecker::getTotalAvailableStockLevel().

starlight-sparkle’s picture

Category: Support request » Feature request

Just noticed that this has since become a feature request, not a support request.

Reviewed with some other devs in an external platform; it makes more sense to update commerce_stock_enforcement_get_ordered_quantity() to use an entity query to retrieve carts including locked carts.