- 🇫🇷France nicolas bouteille
Hello,
I totally agree with this. We are about to use Stripe Checkout on our site which means customers will leave our site right after cart validation and Stripe will handle billing information and payment steps.
We need to lock the order as soon as the customer leaves our site to make sure he cannot update the order once we've communicated the price to be paid to Stripe.
But for the reasons explained above, it can happen that the customer comes back on our website on the cart step.
As mentioned above, the default behavior is to hide locked carts so getting back to /cart will show an empty cart. From this point, adding something to his cart will create a new order while the first order still exists and can be paid.
We too did not like this behavior so here is what we did:We patched CartProvider.php and commented out:
//if ($cart->isLocked()) { //continue; //}
Now when the cart is locked, it still shows up in /cart
This means customers can now interact on a locked cart and change the order, so we had to reinforce the code to make sure that never happens.In CartManager.php, functions emptyCart(), addOrderItem(), removeOrderItem() and updateOrderItem() need to be updated to do nothing (return;) if the $cart->isLocked
For the record, for those who would like to do it on their site while this is not in core, this time, to make it easier to maintain, we did not create a patch, we created a custom class MyModuleCartManager.php that extends CartManager and that overrides only the functions mentioned above. For example:public function emptyCart(OrderInterface $cart, $save_cart = TRUE) { if ($cart->isLocked()) { return; } parent::emptyCart($cart, $save_cart); }
For this custom class to replace CartManager, we created a custom ServiceProvider that extends ServiceProviderBase as explained here:
https://www.drupal.org/docs/drupal-apis/services-and-dependency-injectio... →
Be aware to name it exactly in PascalCase MyModuleNameServiceProvider and place it in my_module/src/.
This is what the code looks like:class MyModuleNameServiceProvider extends ServiceProviderBase { public function alter(ContainerBuilder $container) { $cart_manager = $container->getDefinition('commerce_cart.cart_manager'); $cart_manager->setClass(MyModuleCartManager::class); } }
We also made sure to visually remove the buttons on the cart form that would allow to remove cart items or update quantities...
We also had to do the same for Coupons.
In CouponRedemption.php buildInlineForm() needs to hide the coupon form if the order is locked and validateInlineForm() must stop and show an error in case a coupon is submitted but the order has been locked since.
For this, we also created a custom class MyModuleCouponRedemption.php but this time we used hook_commerce_inline_form_info_alter to replace CouponRedemption with our class:function my_module_commerce_inline_form_info_alter(&$definitions, $b = null, $c = null, $d = null) { if (isset($definitions['coupon_redemption'])) { $definitions['coupon_redemption']['class'] = MyModuleCouponRedemption::class; } }
So now the customer can see his locked cart, but cannot interact with it anymore. We needed to explain to him what was happening.
When the customer is at /cart and his cart is locked, we now display a warning message that says the order is locked and cannot be updated anymore because its price needs to remain what's been communicated to the payment partner.
We also give the opportunity to the customer to definitely cancel this order. So we added a custom button that will cancel the Stripe PaymentIntent and the order. Then the customer sees an empty cart.
The custom code here is added in views-view--commerce_cart_form.html.twig based on new variables passed through hook_preprocess_views_view__commerce_cart_formI understand that this Cancel order button is probably where and why Commerce Core is going to have some trouble implementing all this. Because in OffsitePaymentGatewayInterface there is no function cancelRemotePayment yet. So Commerce Core probably does not have a way to cancel the offsite payment of an order. So if you cannot make sure to properly cancel the order with the remote payment intent as well, you'd better hide the locked cart and allow the customer to start a new cart.
Maybe this cancel button should be visible only under some conditions like the PaymentGateway is not Offsite or the function cancelRemotePayment is implemented… - 🇺🇸United States Erik_MC
Commenting to mention that we are experiencing the same issue on a client site of mine and interested in exploring a solution so that the end user doesn't think their cart is gone.
- 🇬🇧United Kingdom Rob230
I still foresee issues with off-site payment gateways.
For example, one of the issues we have with PayPal payments where the user goes off-site and when they complete payment, get redirected back to Drupal. But the user can just open another tab, make changes to the cart, and then complete the order (for less) and in Drupal it shows up as having different things in the cart which cost more.
OK, so the solution proposed in this ticket is to lock the order when they go off site. But this presents a new problem: user actually doesn't want to proceed with the order and wants to change it, so they close the window or navigate back, but they're locked out.
So we give the user a cancel button allowing them to cancel the order. But what if they use the cancel button in one tab whilst another tab is still open on the external payment page. They then complete the payment, get redirected to Drupal, where the order has been cancelled.
I don't know what can be done about this last scenario. It's user error, but it is something users are capable of and some people will do it (based on my experience!).
- 🇫🇷France nicolas bouteille
Hi Rob,
As I mentioned earlier, the cancel button should not only cancel the Drupal Order but also the remote payment.
We used Stripe on our site so we had to cancel the Stripe remote PaymentIntent.
Now we switched to Stripe Checkout so we now cancel the remote Stripe Checkout Session.
Once it is canceled, the user will receive an error if they try to go proceed with remote payment.
However, as I mentioned as well, this is not yet possible for Commerce Core to implement as of right now because OffsitePaymentGatewayInterface does not have a cancelRemotePayment function yet. Commerce Core does not know which payment gateway is in use and how it should cancel the remote payment session. It should just be able to tell the Offsite Payment Gateway to cancel the remote payment, and the payment gateway should be responsible for cancelling it.
For now this requires custom coding. - 🇮🇱Israel jsacksick
The main reason why locked orders aren't returned / filtered out from the cart provider is we don't really want another add to cart operation to consider this cart, and allow updating the cart content while the customer is supposedly offsite... This would cause a lot of issues (mismatching order content / total etc)...
What we could potentially do is extend getCarts(), getCartIds(), getCartId() and loadCartData() to accept an extra parameter
bool $exclude_locked_orders = TRUE
. This could allow for example the CartController to not excluded locked orders.
At the same time, we'd need the order to be readonly there, but that doesn't really help with checkout as again in this case you don't want to allow any update to the order... This is a tricky problem.