<?php declare(strict_types=1);
namespace Wexo\Relewise\Subscriber;
use Psr\Log\LoggerInterface;
use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\Event\CartChangedEvent;
use Shopware\Core\Checkout\Cart\LineItem\LineItem;
use Shopware\Core\Checkout\Cart\LineItem\LineItemCollection;
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
use Shopware\Core\Checkout\Customer\Event\CustomerLoginEvent;
use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemCollection;
use Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionDefinition;
use Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStates;
use Shopware\Core\Checkout\Order\OrderEntity;
use Shopware\Core\Content\Product\ProductCollection;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\Struct\Collection;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\PlatformRequest;
use Shopware\Core\System\SalesChannel\Context\AbstractSalesChannelContextFactory;
use Shopware\Core\System\SalesChannel\Context\SalesChannelContextService;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\StateMachine\Event\StateMachineTransitionEvent;
use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Wexo\Relewise\Api\Data\Tracking\CartTrackingInterface;
use Wexo\Relewise\Api\Data\Tracking\OrderTrackingInterface;
use Wexo\Relewise\Api\Data\Tracking\ProductTrackingInterface;
use Wexo\Relewise\Api\RelewiseRequestInterface;
use Wexo\Relewise\Core\Content\Relewise\Event\RelewiseCartTrackingEvent;
use Wexo\Relewise\Core\Content\Relewise\Event\RelewiseOrderTrackingEvent;
use Wexo\Relewise\Service\Constant\Constants;
use Wexo\Relewise\Service\Registry\RelewiseModelRegistry;
use Wexo\Relewise\WexoRelewise;
class TrackingSubscriber implements EventSubscriberInterface
{
public const RELEWISE_COOKIE_LIFETIME = 31536000; // A year in seconds.
/**
* @var RelewiseModelRegistry
*/
protected $modelRegistry;
/**
* @var RelewiseRequestInterface
*/
protected $requestService;
/**
* @var LoggerInterface
*/
protected $logger;
/**
* @var EntityRepositoryInterface
*/
protected $productRepository;
/**
* @var AbstractSalesChannelContextFactory
*/
protected $contextFactory;
/**
* @var CartService
*/
protected $cartService;
/**
* @var RequestStack
*/
protected $requestStack;
/**
* @var EntityRepositoryInterface
*/
protected $stateMachineStateRepository;
/**
* @var EntityRepositoryInterface
*/
protected $orderRepository;
/**
* @var EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* @param RelewiseModelRegistry $modelRegistry
* @param RelewiseRequestInterface $requestService
* @param LoggerInterface $logger
* @param EntityRepositoryInterface $productRepository
* @param AbstractSalesChannelContextFactory $contextFactory
* @param CartService $cartService
* @param RequestStack $requestStack
* @param EntityRepositoryInterface $stateMachineStateRepository
* @param EntityRepositoryInterface $orderRepository
* @param EventDispatcherInterface $eventDispatcher
*/
public function __construct(
RelewiseModelRegistry $modelRegistry,
RelewiseRequestInterface $requestService,
LoggerInterface $logger,
EntityRepositoryInterface $productRepository,
AbstractSalesChannelContextFactory $contextFactory,
CartService $cartService,
RequestStack $requestStack,
EntityRepositoryInterface $stateMachineStateRepository,
EntityRepositoryInterface $orderRepository,
EventDispatcherInterface $eventDispatcher
) {
$this->modelRegistry = $modelRegistry;
$this->requestService = $requestService;
$this->logger = $logger;
$this->productRepository = $productRepository;
$this->contextFactory = $contextFactory;
$this->cartService = $cartService;
$this->requestStack = $requestStack;
$this->stateMachineStateRepository = $stateMachineStateRepository;
$this->orderRepository = $orderRepository;
$this->eventDispatcher = $eventDispatcher;
}
/**
* @return string[]
*/
public static function getSubscribedEvents(): array
{
return [
KernelEvents::RESPONSE => 'onResponse',
ProductPageLoadedEvent::class => 'onProductPageLoad',
StateMachineTransitionEvent::class => 'onOrderStateChanged',
CartChangedEvent::class => 'onCartChanged',
CustomerLoginEvent::class => 'onCustomerLogin'
];
}
/**
* @param ResponseEvent $event
*/
public function onResponse(ResponseEvent $event): void
{
$response = $event->getResponse();
$request = $event->getRequest();
// Tracking is not allowed, so we can't save the temporary id as a cookie
if (!RelewiseModelRegistry::isTrackingAllowed($request)) {
return;
}
$context = $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
if (!$context instanceof SalesChannelContext ||
$request->cookies->get(Constants::RELEWISE_COOKIE_TEMP_ID, false)) {
return;
}
$cookie = Cookie::create(
Constants::RELEWISE_COOKIE_TEMP_ID,
$context->getToken(),
time() + self::RELEWISE_COOKIE_LIFETIME
);
$response->headers->setCookie($cookie);
}
/**
* @param ProductPageLoadedEvent $event
*/
public function onProductPageLoad(ProductPageLoadedEvent $event): void
{
// Check if page visitor is http cache warmer
if ($event->getRequest()->headers->get('user-agent') === 'Symfony') {
return;
}
$salesChannelContext = $event->getSalesChannelContext();
$context = $event->getContext();
/** @var ProductTrackingInterface $trackingType */
$trackingType = $this->modelRegistry->createTracking('product', $salesChannelContext, $event->getRequest());
$product = $event->getPage()->getProduct();
if ($parentId = $product->getParentId()) {
$parent = $this->productRepository->search(new Criteria([$parentId]), $context)->first();
if ($parent) {
$trackingType->setVariant($product);
$product = $parent;
}
}
$trackingType->setProduct($product);
$this->requestService->request($trackingType, $context);
}
/**
* @param StateMachineTransitionEvent $event
*/
public function onOrderStateChanged(StateMachineTransitionEvent $event): void
{
if ($event->getEntityName() !== OrderTransactionDefinition::ENTITY_NAME) {
return;
}
$context = $event->getContext();
$stateAuthorizedId = $this->getOrderTransactionStateAuthorized($context);
if ($stateAuthorizedId === null) {
return;
}
if ($event->getToPlace()->getId() !== $stateAuthorizedId) {
return;
}
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('transactions.id', $event->getEntityId()));
$criteria->addAssociation('lineItems');
$criteria->addAssociation('orderCustomer');
/** @var OrderEntity $order */
$order = $this->orderRepository->search($criteria, $context)->first();
$salesChannelContext = $this->contextFactory->create(
Uuid::randomHex(),
$order->getSalesChannelId(),
[
SalesChannelContextService::CUSTOMER_ID => $order->getOrderCustomer()->getCustomerId()
]
);
/** @var OrderTrackingInterface $trackingType */
$trackingType = $this->modelRegistry->createTracking('order', $salesChannelContext);
$lineItems = clone($order->getLineItems())->filterByType(LineItem::PRODUCT_LINE_ITEM_TYPE);
$lineItems = $this->formatTrackingLineItems($lineItems, $salesChannelContext);
$trackingType->setLineItems($lineItems);
$trackingType->setSubtotal(
$order->getPrice()->getTotalPrice(),
$salesChannelContext->getCurrency()->getIsoCode()
);
$trackingType->setTrackingNumber($order->getOrderNumber());
$this->eventDispatcher->dispatch(new RelewiseOrderTrackingEvent(
$trackingType,
$order,
$salesChannelContext
));
$this->requestService->request($trackingType, $context);
}
/**
* @param Context $context
* @return string|null
*/
private function getOrderTransactionStateAuthorized(Context $context): ?string
{
$criteria = new Criteria();
$criteria->addFilter(
new EqualsFilter(
'stateMachine.technicalName',
sprintf(
'%s.state',
OrderTransactionDefinition::ENTITY_NAME
)
),
new EqualsFilter('technicalName', OrderTransactionStates::STATE_AUTHORIZED)
);
return $this->stateMachineStateRepository->searchIds($criteria, $context)->firstId();
}
/**
* @param CartChangedEvent $event
*/
public function onCartChanged(CartChangedEvent $event): void
{
$salesChannelContext = $event->getContext();
$cart = $event->getCart();
$context = $salesChannelContext->getContext();
// Only track saves to the actual cart saved in the session
if ($salesChannelContext->getToken() !== $cart->getToken() || empty($cart->getData()->getElements())) {
return;
}
$trackingType = $this->createCartTracking($cart, $salesChannelContext);
$this->sendCartTracking($trackingType, $cart, $salesChannelContext);
}
/**
* @param CustomerLoginEvent $event
*/
public function onCustomerLogin(CustomerLoginEvent $event): void
{
$request = $this->requestStack->getMainRequest();
$tempId = $request->cookies->get(Constants::RELEWISE_COOKIE_TEMP_ID);
if ($tempId === null) {
return;
}
$salesChannelContext = $event->getSalesChannelContext();
$cart = $this->cartService->getCart($salesChannelContext->getToken(), $salesChannelContext);
$trackingType = $this->createCartTracking($cart, $salesChannelContext);
$trackingType->setUserData([
WexoRelewise::TEMP_ID => $tempId,
WexoRelewise::AUTH_ID => $event->getCustomer()->getId(),
WexoRelewise::USER_EMAIL => $event->getCustomer()->getEmail()
]);
$this->sendCartTracking($trackingType, $cart, $salesChannelContext);
}
/**
* @param CartTrackingInterface $cartTracking
* @param Cart $cart
* @param SalesChannelContext $salesChannelContext
*/
private function sendCartTracking(
CartTrackingInterface $cartTracking,
Cart $cart,
SalesChannelContext $salesChannelContext
): void {
$this->eventDispatcher->dispatch(new RelewiseCartTrackingEvent(
$cartTracking,
$cart,
$salesChannelContext
));
$this->requestService->request($cartTracking, $salesChannelContext->getContext());
}
/**
* @param Cart $cart
* @param SalesChannelContext $salesChannelContext
* @return CartTrackingInterface
*/
protected function createCartTracking(Cart $cart, SalesChannelContext $salesChannelContext): CartTrackingInterface
{
/** @var CartTrackingInterface $trackingType */
$trackingType = $this->modelRegistry->createTracking('cart', $salesChannelContext);
$lineItems = $this->handleSwagCustomizedProducts(clone($cart->getLineItems()));
$lineItems = $this->formatTrackingLineItems($lineItems, $salesChannelContext);
$trackingType->setLineItems($lineItems);
$trackingType->setSubtotal(
$cart->getPrice()->getTotalPrice(),
$salesChannelContext->getCurrency()->getIsoCode()
);
return $trackingType;
}
/**
* @param LineItemCollection|OrderLineItemCollection $lineItems
* @param SalesChannelContext $salesChannelContext
* @return LineItemCollection|OrderLineItemCollection
*/
protected function formatTrackingLineItems($lineItems, SalesChannelContext $salesChannelContext): Collection
{
$context = $salesChannelContext->getContext();
$variants = [];
foreach ($lineItems as $lineItem) {
// If lineItem has options payload value, it's a variant
if (isset($lineItem->getPayload()['options']) && $lineItem->getPayload()['options']) {
$variants[] = $lineItem->getReferencedId();
}
}
// Get all lineItems as products to get the parentId value
$products = $this->productRepository->search(
(new Criteria())->addFilter(new EqualsAnyFilter('id', $variants)),
$context
)->getEntities();
// Get all parent product numbers
/** @var ProductCollection $products */
foreach ($products as $product) {
if ($parentId = $product->getParentId()) {
$parent = $this->productRepository->search(new Criteria([$parentId]), $context)->first();
if ($parent) {
$currentLineItem = $lineItems->get($product->getId());
if ($currentLineItem === null) {
$currentLineItem = $lineItems->filter(function ($item) use ($product) {
return $item->getReferencedId() === $product->getId();
})->first();
}
$payload = $currentLineItem->getPayload();
$payload['parentProduct'] = $parent->getProductNumber();
$currentLineItem->setPayload($payload);
}
}
}
return $lineItems;
}
/**
* @param LineItemCollection $lineItems
* @return LineItemCollection
*/
protected function handleSwagCustomizedProducts(LineItemCollection $lineItems): LineItemCollection
{
$customizedProducts = $lineItems->filterType('customized-products');
foreach ($customizedProducts as $customizedProduct) {
$product = $customizedProduct->getChildren()
->filterType(LineItem::PRODUCT_LINE_ITEM_TYPE)
->first();
$lineItems->add($product);
$lineItems->remove($customizedProduct->getId());
}
return $lineItems;
}
}