custom/plugins/WbfkExtensions/src/Service/TopSellerService.php line 77

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace WbfkExtensions\Service;
  4. use DateInterval;
  5. use DateTime;
  6. use Doctrine\DBAL\Connection;
  7. use Doctrine\DBAL\Exception;
  8. use Shopware\Core\Checkout\Cart\LineItem\LineItem;
  9. use Shopware\Core\Checkout\Order\OrderStates;
  10. use Shopware\Core\Content\Product\Events\ProductListingCriteriaEvent;
  11. use Shopware\Core\Defaults;
  12. use Shopware\Core\Framework\Context;
  13. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\OrFilter;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
  18. use Shopware\Core\Framework\Uuid\Uuid;
  19. use Shopware\Core\System\SystemConfig\Event\SystemConfigChangedEvent;
  20. use Shopware\Core\System\SystemConfig\SystemConfigService;
  21. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  22. use Symfony\Component\Lock\LockFactory;
  23. use Symfony\Component\Messenger\Handler\MessageSubscriberInterface;
  24. use Symfony\Component\Messenger\MessageBusInterface;
  25. use WbfkExtensions\MessageQueue\Message\RecalculateTopSeller;
  26. class TopSellerService implements EventSubscriberInterface, MessageSubscriberInterface
  27. {
  28. public static function getSubscribedEvents(): array
  29. {
  30. return [
  31. SystemConfigChangedEvent::class => 'onSystemConfigChanged',
  32. ProductListingCriteriaEvent::class => 'onProductListingCriteriaEvent',
  33. ];
  34. }
  35. public static function getHandledMessages(): iterable
  36. {
  37. yield RecalculateTopSeller::class => [
  38. 'method' => 'recalculateProductSales',
  39. ];
  40. }
  41. public function __construct(
  42. private readonly SystemConfigService $systemConfigService,
  43. private readonly EntityRepository $salesChannelRepository,
  44. private readonly Connection $connection,
  45. private readonly LockFactory $lockFactory,
  46. private readonly MessageBusInterface $messageBus,
  47. ) {
  48. }
  49. /**
  50. * In the plugin-configuration is specified what durations we are interested in.
  51. * Recalculate the Sales-Numbers if the configuration changed.
  52. */
  53. public function onSystemConfigChanged(SystemConfigChangedEvent $event): void
  54. {
  55. if ($event->getKey() === 'WbfkExtensions.config.topseller') {
  56. $msg = new RecalculateTopSeller($event->getSalesChannelId() ? [$event->getSalesChannelId()] : []);
  57. $lock = $this->lockFactory->createLock($msg->getLockName(), 300, false);
  58. if ($lock->acquire()) {
  59. $this->messageBus->dispatch($msg);
  60. }
  61. }
  62. }
  63. /**
  64. * On loading the products for the product listing we need to apply our BestSeller sorting.
  65. * We encoded the interval in the sorting criteria seperated with the "|" (See js admin component overwrite of "sw-settings-listing-option-criteria-grid")
  66. * Here we 'undo' the postfix and add it as filter for the sorting.
  67. */
  68. public function onProductListingCriteriaEvent(ProductListingCriteriaEvent $event): void
  69. {
  70. $criteria = $event->getCriteria();
  71. $sortings = $criteria->getSorting();
  72. $criteria->resetSorting();
  73. foreach ($sortings as $sorting) {
  74. $tmp = explode('|', $sorting->getField());
  75. if (count($tmp) > 1 && $tmp[0] === 'extensions.wbfkProductSales.sales') {
  76. $criteria->addSorting(new FieldSorting($tmp[0], $sorting->getDirection(), $sorting->getNaturalSorting()));
  77. $criteria->addAssociation('extensions.wbfkProductSales');
  78. $criteria->addFilter(new OrFilter([
  79. new EqualsFilter('extensions.wbfkProductSales.interval', $tmp[1]),
  80. // If there are no sales in the timeframe (null) we still want to show the product in the product listing
  81. new EqualsFilter('extensions.wbfkProductSales.interval', null),
  82. ]));
  83. } else {
  84. $criteria->addSorting($sorting);
  85. }
  86. }
  87. }
  88. /**
  89. * Calculate the sales numbers for every configured interval
  90. *
  91. * @param RecalculateTopSeller $message
  92. * @return void
  93. * @throws \DateInvalidOperationException
  94. * @throws \DateMalformedIntervalStringException
  95. * @throws Exception
  96. */
  97. public function recalculateProductSales(RecalculateTopSeller $message): void
  98. {
  99. $requestedCsIds = $message->salesChannelIds;
  100. $criteria = new Criteria($requestedCsIds ?: null);
  101. $criteria->addFilter(new EqualsFilter('typeId', Defaults::SALES_CHANNEL_TYPE_STOREFRONT));
  102. $context = Context::createDefaultContext();
  103. $salesChannelIds = $this->salesChannelRepository->searchIds($criteria, $context)->getIds();
  104. $sql = <<<SQL
  105. INSERT INTO wbfk_product_sales (id, product_id, product_version_id, sales_channel_id, `interval`, sales, created_at)
  106. SELECT
  107. UNHEX(REPLACE(uuid(),'-','')),
  108. order_line_item.product_id,
  109. :version_id,
  110. :sales_channel_id,
  111. :interval,
  112. SUM(order_line_item.quantity),
  113. NOW()
  114. FROM order_line_item
  115. INNER JOIN `order` ON `order`.id = order_line_item.order_id AND `order`.version_id = order_line_item.order_version_id
  116. INNER JOIN state_machine_state ON state_machine_state.id = `order`.state_id AND state_machine_state.technical_name = :completed_state
  117. WHERE order_line_item.type = :type
  118. AND order_line_item.version_id = :version_id
  119. AND order_line_item.product_id IS NOT NULL
  120. AND `order`.order_date > :from_date
  121. AND `order`.sales_channel_id = :sales_channel_id
  122. GROUP BY product_id;
  123. SQL;
  124. $unitMap = [
  125. 'year' => 'Y',
  126. 'month' => 'M',
  127. 'day' => 'D',
  128. 'week' => 'W',
  129. ];
  130. $now = new DateTime();
  131. foreach ($salesChannelIds as $salesChannelId) {
  132. $this->connection->executeStatement(
  133. 'DELETE FROM wbfk_product_sales WHERE sales_channel_id = :sales_channel_id;',
  134. ['sales_channel_id' => Uuid::fromHexToBytes($salesChannelId)]
  135. );
  136. $topSellerIntervals = $this->systemConfigService->get('WbfkExtensions.config.topseller', $salesChannelId);
  137. foreach ($topSellerIntervals as $topSellerInterval) {
  138. $interval = 'P'.$topSellerInterval['value'].$unitMap[$topSellerInterval['unit']];
  139. $topSellerIntervallStart = $now->sub(new DateInterval($interval));
  140. $this->connection->executeStatement($sql, [
  141. 'sales_channel_id' => Uuid::fromHexToBytes($salesChannelId),
  142. 'version_id' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION),
  143. 'interval' => $interval,
  144. 'type' => LineItem::PRODUCT_LINE_ITEM_TYPE,
  145. 'completed_state' => OrderStates::STATE_COMPLETED,
  146. 'from_date' => $topSellerIntervallStart->format('Y-m-d'),
  147. ]);
  148. }
  149. }
  150. }
  151. }