Hi,
We need to implement abandoned cart retrieval upon login. This is really important feature for us (and fairly standard on decent ecommerce stores), so that customers can return to the site, possibly on another device and continue their journey.
The logic will be that upon login the contents of your most recent abandoned cart will be merged (availability permitting) with your current session cart. The previous abandoned cart is then deleted and you maintain the resulting products going forwards in cart.
We also have a policy of retaining abandoned cart for up to 2 weeks, at which point they are usually deleted for storage reasons. We might potentially change this in future, but it seems reasonable for now.
Has anyone done this or got any ideas on the easiest way to implement it?
Quick thoughts:
We need to replace CartProvider with CompositeCartProvider to allow different cart providers with priorities:
Thoughts?
Thanks for the feedback!
What about the following:
I think there might need to be some Strategy options here so you can configure behaviour because retailers can have very different opinions on this.
We had a look at a few top retail sites and Amazon for one offers merging of session with previous login cart after logging in. Others had varying behaviour but most offered some form of cart retrieval from a previous login.
@adamelso may well end up having a look at this so we might make up a PR based on your outline above and see how it goes... Any further comments & ideas are very welcome though.
We added this feature request on our roadmap a few days ago too :)
We plan to go for the simple implementation of merging the two carts: the one from the Visitor session and the one from the User persistence - no other alternatives.
Though our scenario building phase is still in progress, we don't think there's much business values in the one described below:
Given I am logged in as "John Doe"
And I add product "A" to basket
And I logout
And I add product "B" to basket
When I log in as "John Doe"
And I go to cart I see product "A"
Q: why should product B be removed from my cart if I explicitly added it before logging in? think about sessions that expire: would you like to ask you potential buyers to add to cart twice in 1h if your session lasts only for 30 min. for security reasons?
We plan to implement it like this:
Given I am logged in as "John Doe"
And I add product "A" to basket
And I logout
And I add product "B" to basket
When I log in as "John Doe"
And I go to cart I see the products:
|title|
|product A|
|product B|
Notes: product A are stored in session and in database. We want to use the database to be able to trigger "abandoned cart" transactional emails.
We want to ship this feature in Late March, early April. @peteward how about you?
@gabiudrescu yes, agreed that's the least likely scenario, but this is possible (and implemented by some retailers):
Given I am logged in as "John Doe"
And I add product "A" to basket
And I logout
And I add product "B" to basket
When I log in as "John Doe"
And I go to cart I see product "B"
Because some people consider that if you have a current and active session, that you override the previous one. Although we won't use this strategy,
One scenario that we want to cope with is:
Given I am logged in as "John Doe"
And I add product "A" to basket
And I add product "B" to basket
And I logout
And I add product "B" to basket
And I add product "C" to basket
When I log in as "John Doe"
And I go to cart I see the products with the following quantities:
|title | quantity |
|product A | 1 |
|product B | 1 |
|product C | 1 |
A common behaviour is a customer putting something in their cart on mobile, then returning home and adding it again on their desktop to buy it. They go to checkout and login and find they have a quantity of 2 in their basket. Sometimes they don't even notice and buy 2 accidentally...
So we are looking at a merge-no-duplicates strategy.
Your time-scales sound similar to ours, I would perhaps like this a bit sooner but we'll see.
100% agree with the last behavior - you definitely don't want to item x 2 quantity in you basket upon cart merge.
Will this be implemented soon ? I guess this is quite important for an e-commerce website.
Any news about this feature?
@psihius you mind if I add your solution in a PR?
It's barely a solution at all :)
Here's my class I use, just compare the code with the base class - it's trivial
<?php
/*
* This file is part of the Sylius package.
*
* (c) Pawe艂 J臋drzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace AppBundle\EventListener;
use Doctrine\Common\Persistence\ObjectManager;
use Sylius\Bundle\UserBundle\Event\UserEvent;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\Order\Context\CartContextInterface;
use Sylius\Component\Order\Context\CartNotFoundException;
use Sylius\Component\Resource\Exception\UnexpectedTypeException;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
/**
* @author Micha艂 Marcinkowski <[email protected]>
*/
final class CartBlamerListener
{
/**
* @var ObjectManager
*/
private $cartManager;
/**
* @var CartContextInterface
*/
private $cartContext;
/**
* @var CartContextInterface
*/
private $sessionCartContext;
/**
* @param ObjectManager $cartManager
* @param CartContextInterface $cartContext
* @param CartContextInterface $sessionCartContext
*/
public function __construct(
ObjectManager $cartManager,
CartContextInterface $cartContext,
CartContextInterface $sessionCartContext
) {
$this->cartManager = $cartManager;
$this->cartContext = $cartContext;
$this->sessionCartContext = $sessionCartContext;
}
/**
* @param UserEvent $userEvent
*/
public function onImplicitLogin(UserEvent $userEvent): void
{
$user = $userEvent->getUser();
if (!$user instanceof ShopUserInterface) {
return;
}
$this->blame($user);
}
/**
* @param InteractiveLoginEvent $interactiveLoginEvent
*/
public function onInteractiveLogin(InteractiveLoginEvent $interactiveLoginEvent): void
{
$user = $interactiveLoginEvent->getAuthenticationToken()->getUser();
if (!$user instanceof ShopUserInterface) {
return;
}
$this->blame($user);
}
/**
* @param ShopUserInterface $user
*/
private function blame(ShopUserInterface $user): void
{
$cart = $this->getCart();
if (null === $cart) {
return;
}
try {
$sessionCart = $this->sessionCartContext->getCart();
} catch (CartNotFoundException $e) {
$sessionCart = null;
}
if ($sessionCart !== null && $sessionCart->getId() !== $cart->getId()) {
foreach ($sessionCart->getItems() as $item) {
$cart->addItem($item);
}
$this->cartManager->remove($sessionCart);
$this->cartManager->persist($sessionCart);
} else {
$cart->setCustomer($user->getCustomer());
}
$this->cartManager->persist($cart);
$this->cartManager->flush();
}
/**
* @return OrderInterface|null
*
* @throws UnexpectedTypeException
*/
private function getCart(): ?OrderInterface
{
try {
$cart = $this->cartContext->getCart();
} catch (CartNotFoundException $exception) {
return null;
}
if (!$cart instanceof OrderInterface) {
throw new UnexpectedTypeException($cart, OrderInterface::class);
}
return $cart;
}
}
Thanks a lot @psihius. The default behaviour was preventing us from enabling user login on our website.
From our point of view, on a e-commerce website, there should never be any scenario leading to users losing products in their carts. This should be the standard behaviour in Sylius, or it should at least be possible to activate this through some config.
This issue has been automatically marked as stale because it has not had any recent activity. It will be closed in a week if no further activity occurs. Thank you for your contributions.
I have no idea why this was closed by stalebot and why this isn't yet tackled in Sylius by default.
@CoderMaggie can this task get some product-love, please?
We've implemented the CartBlamerListener by @psihius (https://github.com/Sylius/Sylius/issues/4117#issuecomment-329153002) in our Sylius 1.6 installation but had to also DI the OrderModifierInterface and instead of:
$cart->addItem($item);
we did:
$this->orderModifier->addToOrder($cart, clone $item);
This is the definition in config/services.yaml:
app.listener.cart_blamer:
class: App\EventListener\CartBlamerListener
decorates: sylius.listener.cart_blamer
arguments:
- '@sylius.manager.order'
- '@sylius.context.cart'
- '@sylius.context.cart.session_and_channel_based'
- '@sylius.order_modifier'
Most helpful comment
Thanks a lot @psihius. The default behaviour was preventing us from enabling user login on our website.
From our point of view, on a e-commerce website, there should never be any scenario leading to users losing products in their carts. This should be the standard behaviour in Sylius, or it should at least be possible to activate this through some config.