custom/static-plugins/relewise/src/Subscriber/TrackingSubscriber.php line 313

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Wexo\Relewise\Subscriber;
  3. use Psr\Log\LoggerInterface;
  4. use Shopware\Core\Checkout\Cart\Cart;
  5. use Shopware\Core\Checkout\Cart\Event\CartChangedEvent;
  6. use Shopware\Core\Checkout\Cart\LineItem\LineItem;
  7. use Shopware\Core\Checkout\Cart\LineItem\LineItemCollection;
  8. use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
  9. use Shopware\Core\Checkout\Customer\Event\CustomerLoginEvent;
  10. use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemCollection;
  11. use Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionDefinition;
  12. use Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStates;
  13. use Shopware\Core\Checkout\Order\OrderEntity;
  14. use Shopware\Core\Content\Product\ProductCollection;
  15. use Shopware\Core\Framework\Context;
  16. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  20. use Shopware\Core\Framework\Struct\Collection;
  21. use Shopware\Core\Framework\Uuid\Uuid;
  22. use Shopware\Core\PlatformRequest;
  23. use Shopware\Core\System\SalesChannel\Context\AbstractSalesChannelContextFactory;
  24. use Shopware\Core\System\SalesChannel\Context\SalesChannelContextService;
  25. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  26. use Shopware\Core\System\StateMachine\Event\StateMachineTransitionEvent;
  27. use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
  28. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  29. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  30. use Symfony\Component\HttpFoundation\Cookie;
  31. use Symfony\Component\HttpFoundation\RequestStack;
  32. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  33. use Symfony\Component\HttpKernel\KernelEvents;
  34. use Wexo\Relewise\Api\Data\Tracking\CartTrackingInterface;
  35. use Wexo\Relewise\Api\Data\Tracking\OrderTrackingInterface;
  36. use Wexo\Relewise\Api\Data\Tracking\ProductTrackingInterface;
  37. use Wexo\Relewise\Api\RelewiseRequestInterface;
  38. use Wexo\Relewise\Core\Content\Relewise\Event\RelewiseCartTrackingEvent;
  39. use Wexo\Relewise\Core\Content\Relewise\Event\RelewiseOrderTrackingEvent;
  40. use Wexo\Relewise\Service\Constant\Constants;
  41. use Wexo\Relewise\Service\Registry\RelewiseModelRegistry;
  42. use Wexo\Relewise\WexoRelewise;
  43. class TrackingSubscriber implements EventSubscriberInterface
  44. {
  45.     public const RELEWISE_COOKIE_LIFETIME 31536000// A year in seconds.
  46.     /**
  47.      * @var RelewiseModelRegistry
  48.      */
  49.     protected $modelRegistry;
  50.     /**
  51.      * @var RelewiseRequestInterface
  52.      */
  53.     protected $requestService;
  54.     /**
  55.      * @var LoggerInterface
  56.      */
  57.     protected $logger;
  58.     /**
  59.      * @var EntityRepositoryInterface
  60.      */
  61.     protected $productRepository;
  62.     /**
  63.      * @var AbstractSalesChannelContextFactory
  64.      */
  65.     protected $contextFactory;
  66.     /**
  67.      * @var CartService
  68.      */
  69.     protected $cartService;
  70.     /**
  71.      * @var RequestStack
  72.      */
  73.     protected $requestStack;
  74.     /**
  75.      * @var EntityRepositoryInterface
  76.      */
  77.     protected $stateMachineStateRepository;
  78.     /**
  79.      * @var EntityRepositoryInterface
  80.      */
  81.     protected $orderRepository;
  82.     /**
  83.      * @var EventDispatcherInterface
  84.      */
  85.     protected $eventDispatcher;
  86.     /**
  87.      * @param RelewiseModelRegistry $modelRegistry
  88.      * @param RelewiseRequestInterface $requestService
  89.      * @param LoggerInterface $logger
  90.      * @param EntityRepositoryInterface $productRepository
  91.      * @param AbstractSalesChannelContextFactory $contextFactory
  92.      * @param CartService $cartService
  93.      * @param RequestStack $requestStack
  94.      * @param EntityRepositoryInterface $stateMachineStateRepository
  95.      * @param EntityRepositoryInterface $orderRepository
  96.      * @param EventDispatcherInterface $eventDispatcher
  97.      */
  98.     public function __construct(
  99.         RelewiseModelRegistry $modelRegistry,
  100.         RelewiseRequestInterface $requestService,
  101.         LoggerInterface $logger,
  102.         EntityRepositoryInterface $productRepository,
  103.         AbstractSalesChannelContextFactory $contextFactory,
  104.         CartService $cartService,
  105.         RequestStack $requestStack,
  106.         EntityRepositoryInterface $stateMachineStateRepository,
  107.         EntityRepositoryInterface $orderRepository,
  108.         EventDispatcherInterface $eventDispatcher
  109.     ) {
  110.         $this->modelRegistry $modelRegistry;
  111.         $this->requestService $requestService;
  112.         $this->logger $logger;
  113.         $this->productRepository $productRepository;
  114.         $this->contextFactory $contextFactory;
  115.         $this->cartService $cartService;
  116.         $this->requestStack $requestStack;
  117.         $this->stateMachineStateRepository $stateMachineStateRepository;
  118.         $this->orderRepository $orderRepository;
  119.         $this->eventDispatcher $eventDispatcher;
  120.     }
  121.     /**
  122.      * @return string[]
  123.      */
  124.     public static function getSubscribedEvents(): array
  125.     {
  126.         return [
  127.             KernelEvents::RESPONSE => 'onResponse',
  128.             ProductPageLoadedEvent::class => 'onProductPageLoad',
  129.             StateMachineTransitionEvent::class => 'onOrderStateChanged',
  130.             CartChangedEvent::class => 'onCartChanged',
  131.             CustomerLoginEvent::class => 'onCustomerLogin'
  132.         ];
  133.     }
  134.     /**
  135.      * @param ResponseEvent $event
  136.      */
  137.     public function onResponse(ResponseEvent $event): void
  138.     {
  139.         $response $event->getResponse();
  140.         $request $event->getRequest();
  141.         // Tracking is not allowed, so we can't save the temporary id as a cookie
  142.         if (!RelewiseModelRegistry::isTrackingAllowed($request)) {
  143.             return;
  144.         }
  145.         $context $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
  146.         if (!$context instanceof SalesChannelContext ||
  147.             $request->cookies->get(Constants::RELEWISE_COOKIE_TEMP_IDfalse)) {
  148.             return;
  149.         }
  150.         $cookie Cookie::create(
  151.             Constants::RELEWISE_COOKIE_TEMP_ID,
  152.             $context->getToken(),
  153.             time() + self::RELEWISE_COOKIE_LIFETIME
  154.         );
  155.         $response->headers->setCookie($cookie);
  156.     }
  157.     /**
  158.      * @param ProductPageLoadedEvent $event
  159.      */
  160.     public function onProductPageLoad(ProductPageLoadedEvent $event): void
  161.     {
  162.         // Check if page visitor is http cache warmer
  163.         if ($event->getRequest()->headers->get('user-agent') === 'Symfony') {
  164.             return;
  165.         }
  166.         $salesChannelContext $event->getSalesChannelContext();
  167.         $context $event->getContext();
  168.         /** @var ProductTrackingInterface $trackingType */
  169.         $trackingType $this->modelRegistry->createTracking('product'$salesChannelContext$event->getRequest());
  170.         $product $event->getPage()->getProduct();
  171.         if ($parentId $product->getParentId()) {
  172.             $parent $this->productRepository->search(new Criteria([$parentId]), $context)->first();
  173.             if ($parent) {
  174.                 $trackingType->setVariant($product);
  175.                 $product $parent;
  176.             }
  177.         }
  178.         $trackingType->setProduct($product);
  179.         $this->requestService->request($trackingType$context);
  180.     }
  181.     /**
  182.      * @param StateMachineTransitionEvent $event
  183.      */
  184.     public function onOrderStateChanged(StateMachineTransitionEvent $event): void
  185.     {
  186.         if ($event->getEntityName() !== OrderTransactionDefinition::ENTITY_NAME) {
  187.             return;
  188.         }
  189.         $context $event->getContext();
  190.         $stateAuthorizedId $this->getOrderTransactionStateAuthorized($context);
  191.         if ($stateAuthorizedId === null) {
  192.             return;
  193.         }
  194.         if ($event->getToPlace()->getId() !== $stateAuthorizedId) {
  195.             return;
  196.         }
  197.         $criteria = new Criteria();
  198.         $criteria->addFilter(new EqualsFilter('transactions.id'$event->getEntityId()));
  199.         $criteria->addAssociation('lineItems');
  200.         $criteria->addAssociation('orderCustomer');
  201.         /** @var OrderEntity $order */
  202.         $order $this->orderRepository->search($criteria$context)->first();
  203.         $salesChannelContext $this->contextFactory->create(
  204.             Uuid::randomHex(),
  205.             $order->getSalesChannelId(),
  206.             [
  207.                 SalesChannelContextService::CUSTOMER_ID => $order->getOrderCustomer()->getCustomerId()
  208.             ]
  209.         );
  210.         /** @var OrderTrackingInterface $trackingType */
  211.         $trackingType $this->modelRegistry->createTracking('order'$salesChannelContext);
  212.         $lineItems = clone($order->getLineItems())->filterByType(LineItem::PRODUCT_LINE_ITEM_TYPE);
  213.         $lineItems $this->formatTrackingLineItems($lineItems$salesChannelContext);
  214.         $trackingType->setLineItems($lineItems);
  215.         $trackingType->setSubtotal(
  216.             $order->getPrice()->getTotalPrice(),
  217.             $salesChannelContext->getCurrency()->getIsoCode()
  218.         );
  219.         $trackingType->setTrackingNumber($order->getOrderNumber());
  220.         $this->eventDispatcher->dispatch(new RelewiseOrderTrackingEvent(
  221.             $trackingType,
  222.             $order,
  223.             $salesChannelContext
  224.         ));
  225.         $this->requestService->request($trackingType$context);
  226.     }
  227.     /**
  228.      * @param Context $context
  229.      * @return string|null
  230.      */
  231.     private function getOrderTransactionStateAuthorized(Context $context): ?string
  232.     {
  233.         $criteria = new Criteria();
  234.         $criteria->addFilter(
  235.             new EqualsFilter(
  236.                 'stateMachine.technicalName',
  237.                 sprintf(
  238.                     '%s.state',
  239.                     OrderTransactionDefinition::ENTITY_NAME
  240.                 )
  241.             ),
  242.             new EqualsFilter('technicalName'OrderTransactionStates::STATE_AUTHORIZED)
  243.         );
  244.         return $this->stateMachineStateRepository->searchIds($criteria$context)->firstId();
  245.     }
  246.     /**
  247.      * @param CartChangedEvent $event
  248.      */
  249.     public function onCartChanged(CartChangedEvent $event): void
  250.     {
  251.         $salesChannelContext $event->getContext();
  252.         $cart $event->getCart();
  253.         $context $salesChannelContext->getContext();
  254.         // Only track saves to the actual cart saved in the session
  255.         if ($salesChannelContext->getToken() !== $cart->getToken() || empty($cart->getData()->getElements())) {
  256.             return;
  257.         }
  258.         $trackingType $this->createCartTracking($cart$salesChannelContext);
  259.         $this->sendCartTracking($trackingType$cart$salesChannelContext);
  260.     }
  261.     /**
  262.      * @param CustomerLoginEvent $event
  263.      */
  264.     public function onCustomerLogin(CustomerLoginEvent $event): void
  265.     {
  266.         $request $this->requestStack->getMainRequest();
  267.         $tempId $request->cookies->get(Constants::RELEWISE_COOKIE_TEMP_ID);
  268.         if ($tempId === null) {
  269.             return;
  270.         }
  271.         $salesChannelContext $event->getSalesChannelContext();
  272.         $cart $this->cartService->getCart($salesChannelContext->getToken(), $salesChannelContext);
  273.         $trackingType $this->createCartTracking($cart$salesChannelContext);
  274.         $trackingType->setUserData([
  275.             WexoRelewise::TEMP_ID => $tempId,
  276.             WexoRelewise::AUTH_ID => $event->getCustomer()->getId(),
  277.             WexoRelewise::USER_EMAIL => $event->getCustomer()->getEmail()
  278.         ]);
  279.         $this->sendCartTracking($trackingType$cart$salesChannelContext);
  280.     }
  281.     /**
  282.      * @param CartTrackingInterface $cartTracking
  283.      * @param Cart $cart
  284.      * @param SalesChannelContext $salesChannelContext
  285.      */
  286.     private function sendCartTracking(
  287.         CartTrackingInterface $cartTracking,
  288.         Cart $cart,
  289.         SalesChannelContext $salesChannelContext
  290.     ): void {
  291.         $this->eventDispatcher->dispatch(new RelewiseCartTrackingEvent(
  292.             $cartTracking,
  293.             $cart,
  294.             $salesChannelContext
  295.         ));
  296.         $this->requestService->request($cartTracking$salesChannelContext->getContext());
  297.     }
  298.     /**
  299.      * @param Cart $cart
  300.      * @param SalesChannelContext $salesChannelContext
  301.      * @return CartTrackingInterface
  302.      */
  303.     protected function createCartTracking(Cart $cartSalesChannelContext $salesChannelContext): CartTrackingInterface
  304.     {
  305.         /** @var CartTrackingInterface $trackingType */
  306.         $trackingType $this->modelRegistry->createTracking('cart'$salesChannelContext);
  307.         $lineItems $this->handleSwagCustomizedProducts(clone($cart->getLineItems()));
  308.         $lineItems $this->formatTrackingLineItems($lineItems$salesChannelContext);
  309.         $trackingType->setLineItems($lineItems);
  310.         $trackingType->setSubtotal(
  311.             $cart->getPrice()->getTotalPrice(),
  312.             $salesChannelContext->getCurrency()->getIsoCode()
  313.         );
  314.         return $trackingType;
  315.     }
  316.     /**
  317.      * @param LineItemCollection|OrderLineItemCollection $lineItems
  318.      * @param SalesChannelContext $salesChannelContext
  319.      * @return LineItemCollection|OrderLineItemCollection
  320.      */
  321.     protected function formatTrackingLineItems($lineItemsSalesChannelContext $salesChannelContext): Collection
  322.     {
  323.         $context $salesChannelContext->getContext();
  324.         $variants = [];
  325.         foreach ($lineItems as $lineItem) {
  326.             // If lineItem has options payload value, it's a variant
  327.             if (isset($lineItem->getPayload()['options']) && $lineItem->getPayload()['options']) {
  328.                 $variants[] = $lineItem->getReferencedId();
  329.             }
  330.         }
  331.         // Get all lineItems as products to get the parentId value
  332.         $products $this->productRepository->search(
  333.             (new Criteria())->addFilter(new EqualsAnyFilter('id'$variants)),
  334.             $context
  335.         )->getEntities();
  336.         // Get all parent product numbers
  337.         /** @var ProductCollection $products */
  338.         foreach ($products as $product) {
  339.             if ($parentId $product->getParentId()) {
  340.                 $parent $this->productRepository->search(new Criteria([$parentId]), $context)->first();
  341.                 if ($parent) {
  342.                     $currentLineItem $lineItems->get($product->getId());
  343.                     if ($currentLineItem === null) {
  344.                         $currentLineItem $lineItems->filter(function ($item) use ($product) {
  345.                             return $item->getReferencedId() === $product->getId();
  346.                         })->first();
  347.                     }
  348.                     $payload $currentLineItem->getPayload();
  349.                     $payload['parentProduct'] = $parent->getProductNumber();
  350.                     $currentLineItem->setPayload($payload);
  351.                 }
  352.             }
  353.         }
  354.         return $lineItems;
  355.     }
  356.     /**
  357.      * @param LineItemCollection $lineItems
  358.      * @return LineItemCollection
  359.      */
  360.     protected function handleSwagCustomizedProducts(LineItemCollection $lineItems): LineItemCollection
  361.     {
  362.         $customizedProducts $lineItems->filterType('customized-products');
  363.         foreach ($customizedProducts as $customizedProduct) {
  364.             $product $customizedProduct->getChildren()
  365.                 ->filterType(LineItem::PRODUCT_LINE_ITEM_TYPE)
  366.                 ->first();
  367.             $lineItems->add($product);
  368.             $lineItems->remove($customizedProduct->getId());
  369.         }
  370.         return $lineItems;
  371.     }
  372. }