<?php
namespace NewsletterSendinblue\Subscriber;
use Doctrine\DBAL\Connection;
use NewsletterSendinblue\Core\Content\Cart\AbandonedCartEntity;
use NewsletterSendinblue\Service\Cart\CartEventProducer;
use NewsletterSendinblue\Service\ConfigService;
use NewsletterSendinblue\Service\Cart\SendinblueCartProcessorService;
use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\AbstractCartPersister;
use Shopware\Core\Checkout\Cart\Event\AfterLineItemAddedEvent;
use Shopware\Core\Checkout\Cart\Event\AfterLineItemQuantityChangedEvent;
use Shopware\Core\Checkout\Cart\Event\AfterLineItemRemovedEvent;
use Shopware\Core\Checkout\Cart\Event\BeforeCartMergeEvent;
use Shopware\Core\Checkout\Cart\Event\BeforeLineItemAddedEvent;
use Shopware\Core\Checkout\Cart\Event\BeforeLineItemQuantityChangedEvent;
use Shopware\Core\Checkout\Cart\Event\BeforeLineItemRemovedEvent;
use Shopware\Core\Checkout\Cart\Event\LineItemAddedEVent;
use Shopware\Core\Checkout\Cart\Event\LineItemQuantityChangedEvent;
use Shopware\Core\Checkout\Cart\Event\LineItemRemovedEvent;
use Shopware\Core\Checkout\Cart\LineItem\LineItemCollection;
use Shopware\Core\Checkout\Cart\Order\CartConvertedEvent;
use Shopware\Core\Checkout\Customer\CustomerEntity;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Page\Checkout\Cart\CheckoutCartPageLoadedEvent;
use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Throwable;
class AbandonedCartSubscriber implements EventSubscriberInterface
{
/**
* @var CartEventProducer
*/
private $cartEventProducer;
/**
* @var SendinblueCartProcessorService
*/
private $cartProcessorService;
/**
* @var ConfigService
*/
private $configService;
/**
* @var AbstractCartPersister
*/
private $cartPersister;
/**
* @var Connection
*/
private $connection;
/**
* @var EntityRepositoryInterface
*/
private $brevoAbandonedCartRepository;
/**
* @var RequestStack;
*/
private $requestStack;
/**
* AbandonedCartSubscriber constructor.
*
* @param CartEventProducer $cartEventProducer
* @param SendinblueCartProcessorService $cartProcessorService
* @param ConfigService $configService
* @param AbstractCartPersister $cartPersister
* @param Connection $connection
* @param EntityRepositoryInterface $brevoAbandonedCartRepository
* @param RequestStack $requestStack
*/
public function __construct(
CartEventProducer $cartEventProducer,
SendinblueCartProcessorService $cartProcessorService,
ConfigService $configService,
AbstractCartPersister $cartPersister,
Connection $connection,
EntityRepositoryInterface $brevoAbandonedCartRepository,
RequestStack $requestStack
)
{
$this->cartEventProducer = $cartEventProducer;
$this->cartProcessorService = $cartProcessorService;
$this->configService = $configService;
$this->cartPersister = $cartPersister;
$this->connection = $connection;
$this->brevoAbandonedCartRepository = $brevoAbandonedCartRepository;
$this->requestStack = $requestStack;
}
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
$events = [];
$events[CartConvertedEvent::class] = 'onCartConvertedEvent';
$events[CheckoutCartPageLoadedEvent::class] = 'onCheckoutCartPageLoadedEvent';
$events[CheckoutConfirmPageLoadedEvent::class] = 'onCheckoutConfirmPageLoadedEvent';
$events[BeforeCartMergeEvent::class] = 'onBeforeCartMergeEvent';
if (class_exists('\Shopware\Core\Checkout\Cart\Event\BeforeLineItemAddedEvent')) {
// for newer shopware versions
$events[BeforeLineItemAddedEvent::class] = 'onCartUpdatedEvent';
$events[BeforeLineItemQuantityChangedEvent::class] = 'onCartUpdatedEvent';
$events[BeforeLineItemRemovedEvent::class] = 'onCartUpdatedEvent';
$events[AfterLineItemAddedEvent::class] = 'onAfterCartUpdatedEvent';
$events[AfterLineItemQuantityChangedEvent::class] = 'onAfterCartUpdatedEvent';
$events[AfterLineItemRemovedEvent::class] = 'onAfterCartUpdatedEvent';
} else if (class_exists('\Shopware\Core\Checkout\Cart\Event\LineItemAddedEvent')) {
// for older shopware versions
$events[LineItemAddedEvent::class] = 'onCartUpdatedEvent';
$events[LineItemQuantityChangedEvent::class] = 'onCartUpdatedEvent';
$events[LineItemRemovedEvent::class] = 'onCartUpdatedEvent';
}
return $events;
}
/**
* @param CheckoutConfirmPageLoadedEvent $event
* @return void
*/
public function onCheckoutConfirmPageLoadedEvent(CheckoutConfirmPageLoadedEvent $event)
{
if (method_exists($event, 'getSalesChannelContext') && $event->getSalesChannelContext() instanceof SalesChannelContext) {
$salesChannelId = $event->getSalesChannelContext()->getSalesChannelId();
} else if (method_exists($event, 'getContext') && $event->getContext() instanceof SalesChannelContext) {
$salesChannelId = $event->getContext()->getSalesChannelId();
} else {
return;
}
$this->configService->setSalesChannelId($salesChannelId);
if ($this->configService->isAbandonedCartTrackingEnabled()) {
$customer = $event->getSalesChannelContext()->getCustomer();
if (!$customer instanceof CustomerEntity
|| !$event->getPage()->getCart() instanceof Cart
) {
return;
}
$payload = $this->getCartPayload($event->getPage()->getCart(), $event->getSalesChannelContext());
try {
$this->brevoAbandonedCartRepository->upsert([[
'id' => md5($customer->getEmail()),
'email' => $customer->getEmail(),
'payload' => $payload,
]], $event->getContext());
} catch (Throwable $e) {
}
}
}
/**
* @param BeforeCartMergeEvent $event
* @return void
*/
public function onBeforeCartMergeEvent(BeforeCartMergeEvent $event)
{
if ($this->requestStack
&& $this->requestStack->getCurrentRequest()
&& $this->requestStack->getCurrentRequest()->hasSession()
&& $this->requestStack->getSession()->has('sbAbandonedCartIsUsed')
) {
// to avoid duplicates in the cart
$event->getCustomerCart()->setLineItems(new LineItemCollection());
$this->requestStack->getSession()->remove('sbAbandonedCartIsUsed');
}
}
/**
* @param CheckoutCartPageLoadedEvent $event
* @return void
*/
public function onCheckoutCartPageLoadedEvent(CheckoutCartPageLoadedEvent $event)
{
$bacId = $event->getRequest()->get('bacId', '');
if (!Uuid::isValid($bacId)
|| !$event->getSalesChannelContext() instanceof SalesChannelContext
|| $event->getSalesChannelContext()->getCustomer() instanceof CustomerEntity
) {
return;
}
if (!$this->requestStack
|| !$this->requestStack->getCurrentRequest()
|| !$this->requestStack->getCurrentRequest()->hasSession()
) {
return;
}
// It means it's already loaded from the abandoned cart
if ($this->requestStack->getSession()->has('sbAbandonedCartIsUsed')) {
return;
}
$criteria = new Criteria([$bacId]);
$brevoAbandonedCart = $this->brevoAbandonedCartRepository->search($criteria, $event->getContext())->first();
if (!$brevoAbandonedCart instanceof AbandonedCartEntity) {
return;
}
$abandonedCart = unserialize($brevoAbandonedCart->getPayload());
if (!$abandonedCart instanceof Cart) {
$this->brevoAbandonedCartRepository->delete([
['id' => $bacId]
], $event->getSalesChannelContext()->getContext());
return;
}
$abandonedCart->setToken($event->getSalesChannelContext()->getToken());
$this->cartPersister->save($abandonedCart, $event->getSalesChannelContext());
$event->getPage()->setCart($abandonedCart);
$event->getRequest()->getSession()->set('sbAbandonedCartIsUsed', true);
}
/**
* @param CartConvertedEvent $event
*/
public function onCartConvertedEvent(CartConvertedEvent $event): void
{
$this->configService->setSalesChannelId($event->getSalesChannelContext()->getSalesChannelId());
if ($this->configService->isAbandonedCartTrackingEnabled()) {
$this->cartEventProducer->processOrder(
$event->getOriginalConvertedCart(),
$event->getCart(),
$event->getSalesChannelContext()
);
}
}
/**
* @param $event
* @return void
*/
public function onAfterCartUpdatedEvent($event)
{
if (method_exists($event, 'getSalesChannelContext') && $event->getSalesChannelContext() instanceof SalesChannelContext) {
$salesChannelId = $event->getSalesChannelContext()->getSalesChannelId();
} else if (method_exists($event, 'getContext') && $event->getContext() instanceof SalesChannelContext) {
$salesChannelId = $event->getContext()->getSalesChannelId();
} else {
return;
}
$this->configService->setSalesChannelId($salesChannelId);
if ($this->configService->isAbandonedCartTrackingEnabled()) {
$customer = $event->getSalesChannelContext()->getCustomer();
if (!$customer instanceof CustomerEntity
|| !$event->getCart() instanceof Cart
) {
return;
}
$payload = $this->getCartPayload($event->getCart(), $event->getSalesChannelContext());
try {
$this->brevoAbandonedCartRepository->upsert([[
'id' => md5($customer->getEmail()),
'email' => $customer->getEmail(),
'payload' => $payload,
]], $event->getContext());
} catch (Throwable $e) {
}
}
}
/**
* @param $event
*/
public function onCartUpdatedEvent($event): void
{
if (method_exists($event, 'getSalesChannelContext') && $event->getSalesChannelContext() instanceof SalesChannelContext) {
$salesChannelId = $event->getSalesChannelContext()->getSalesChannelId();
} else if (method_exists($event, 'getContext') && $event->getContext() instanceof SalesChannelContext) {
$salesChannelId = $event->getContext()->getSalesChannelId();
} else {
return;
}
$this->configService->setSalesChannelId($salesChannelId);
if ($this->configService->isAbandonedCartTrackingEnabled()) {
$this->cartProcessorService->setShouldCollectData(true);
}
}
/**
* @param Cart $cart
* @param SalesChannelContext $salesChannelContext
* @return string|null
*/
private function getCartPayload(Cart $cart, SalesChannelContext $salesChannelContext): ?string
{
$payload = null;
try {
if ($cart->getLineItems()->count() > 0) {
$cartContent = $this->connection->fetchAssociative(
'#cart-persister::load
SELECT `cart`.`payload`, `cart`.`rule_ids`, `cart`.`compressed` FROM cart WHERE `token` = :token',
['token' => $salesChannelContext->getToken()]
);
if (!empty($cartContent)) {
$payload = $cartContent['payload'];
}
}
} catch (Throwable $e) {
}
return $payload;
}
}