custom/plugins/TwigelGetNotified/src/Service/StockSubscriptionService.php line 49

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Twigel\GetNotified\Service;
  4. use Shopware\Core\Checkout\Customer\CustomerEntity;
  5. use Shopware\Core\Content\Product\ProductEntity;
  6. use Shopware\Core\Framework\Context;
  7. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenContainerEvent;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  10. use Shopware\Core\Framework\Uuid\Uuid;
  11. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  12. use Shopware\Core\System\SalesChannel\SalesChannelEntity;
  13. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  14. use Symfony\Component\Messenger\MessageBusInterface;
  15. use Twigel\GetNotified\Content\StockSubscriber\Aggregate\StockSubscriberProduct;
  16. use Twigel\GetNotified\Content\StockSubscriber\Aggregate\StockSubscriberProductCollection;
  17. use Twigel\GetNotified\Content\StockSubscriber\Aggregate\StockSubscriberProductRepository;
  18. use Twigel\GetNotified\Content\StockSubscriber\StockSubscriber;
  19. use Twigel\GetNotified\Content\StockSubscriber\StockSubscriberRepository;
  20. use Twigel\GetNotified\MessageBus\Message\DoubleOptInMessage;
  21. use Doctrine\DBAL\Connection;
  22. /**
  23.  * Class StockSubscriptionService
  24.  *
  25.  * @package Twigel\GetNotified\Service
  26.  */
  27. class StockSubscriptionService
  28. {
  29.     const ACTION_SUBSCRIPTION_CREATED 'subscription_created';
  30.     const ACTION_SUBSCRIPTION_UPDATED 'subscription_updated';
  31.     protected EntityRepositoryInterface $productRepository;
  32.     protected StockSubscriberRepository $stockSubscriberRepository;
  33.     protected StockSubscriberProductRepository $stockSubscriberProductRepository;
  34.     protected ConfigService $configService;
  35.     protected MailService $mailService;
  36.     protected MessageBusInterface $messageBus;
  37.     protected ?EventDispatcherInterface $flowDispatcher;
  38.     /**
  39.      * @var Connection
  40.      */
  41.     private Connection $connection;
  42.     public function __construct(
  43.         EntityRepositoryInterface        $productRepository,
  44.         StockSubscriberRepository        $stockSubscriberRepository,
  45.         StockSubscriberProductRepository $stockSubscriberProductRepository,
  46.         ConfigService                    $configService,
  47.         MailService                      $mailService,
  48.         MessageBusInterface              $messageBus,
  49.         ?EventDispatcherInterface        $flowDispatcher null,
  50.         Connection                       $connection
  51.     )
  52.     {
  53.         $this->productRepository $productRepository;
  54.         $this->stockSubscriberRepository $stockSubscriberRepository;
  55.         $this->stockSubscriberProductRepository $stockSubscriberProductRepository;
  56.         $this->configService $configService;
  57.         $this->mailService $mailService;
  58.         $this->messageBus $messageBus;
  59.         $this->flowDispatcher $flowDispatcher;
  60.         $this->connection $connection;
  61.     }
  62.     /**
  63.      * @param SalesChannelContext $context
  64.      * @param bool $isListing
  65.      *
  66.      * @return bool
  67.      */
  68.     public function shouldShowSubscriptionForm(SalesChannelContext $contextbool $isListing false): bool
  69.     {
  70.         $salesChannel $context->getSalesChannel();
  71.         if (!$this->isActive($salesChannel)) {
  72.             return false;
  73.         }
  74.         $customerLoggedIn $context->getCustomer() instanceof CustomerEntity;
  75.         $loginRequired $this->configService->getHideIfNotLoggedIn($salesChannel->getId());
  76.         if (!$customerLoggedIn && $loginRequired) {
  77.             return false;
  78.         }
  79.         $customerGroupWhiteList $this->configService->getCustomerGroupWhitelist($salesChannel->getId());
  80.         $currentCustomerGroupId $context->getCurrentCustomerGroup()->getId();
  81.         if (
  82.             $loginRequired &&
  83.             !empty($customerGroupWhiteList) &&
  84.             !in_array($currentCustomerGroupId$customerGroupWhiteList)
  85.         ) {
  86.             return false;
  87.         }
  88.         return true;
  89.     }
  90.     /**
  91.      * @param SalesChannelEntity $salesChannelEntity
  92.      *
  93.      * @return bool
  94.      */
  95.     public function isActive(SalesChannelEntity $salesChannelEntity): bool
  96.     {
  97.         return $this->configService->getActive($salesChannelEntity);
  98.     }
  99.     /**
  100.      * @param SalesChannelEntity $salesChannelEntity
  101.      *
  102.      * @return bool
  103.      */
  104.     public function doubleOptInEnabled(SalesChannelEntity $salesChannelEntity): bool
  105.     {
  106.         return $this->configService->getDoubleOptInEnabled($salesChannelEntity);
  107.     }
  108.     /**
  109.      * @param SalesChannelEntity $salesChannelEntity
  110.      *
  111.      * @return bool
  112.      */
  113.     public function useFlowBuilder(?SalesChannelEntity $salesChannelEntity null): bool
  114.     {
  115.         return $this->configService->getUseFlowBuilder($salesChannelEntity) && $this->flowDispatcher !== null;
  116.     }
  117.     /**
  118.      * @param SalesChannelEntity|null $salesChannelEntity
  119.      * @param bool $clear
  120.      */
  121.     public function setLastRunTimestamp(?SalesChannelEntity $salesChannelEntity nullbool $clear false): void
  122.     {
  123.         $this->configService->setLastRunTimestamp($salesChannelEntity$clear);
  124.     }
  125.     /**
  126.      * @param SalesChannelEntity|null $salesChannelEntity
  127.      *
  128.      * @return int
  129.      */
  130.     public function getNotificationBatchSize(?SalesChannelEntity $salesChannelEntity null): int
  131.     {
  132.         return $this->configService->getNotificationBatchSize($salesChannelEntity);
  133.     }
  134.     /**
  135.      * @param SalesChannelEntity $salesChannelEntity
  136.      *
  137.      * @return bool
  138.      */
  139.     public function getHideStockSubscriptionFormOnListingPage(SalesChannelEntity $salesChannelEntity): bool
  140.     {
  141.         return $this->configService->getHideStockSubscriptionFormOnListingPage($salesChannelEntity);
  142.     }
  143.     /**
  144.      * @param SalesChannelEntity $salesChannelEntity
  145.      *
  146.      * @return bool
  147.      */
  148.     public function shouldReplaceHideAddToCartForm(SalesChannelEntity $salesChannelEntity): bool
  149.     {
  150.         return $this->configService->getReplaceAddToCart($salesChannelEntity);
  151.     }
  152.     /**
  153.      * @param SalesChannelEntity $salesChannelEntity
  154.      *
  155.      * @return bool
  156.      */
  157.     public function showNotificationBell(SalesChannelEntity $salesChannelEntity): bool
  158.     {
  159.         return $this->configService->getShowNotificationBell($salesChannelEntity);
  160.     }
  161.     /**
  162.      * @param SalesChannelEntity $salesChannelEntity
  163.      *
  164.      * @return bool
  165.      */
  166.     public function showTitle(SalesChannelEntity $salesChannelEntity): bool
  167.     {
  168.         return $this->configService->getShowTitle($salesChannelEntity->getId());
  169.     }
  170.     /**
  171.      * @param SalesChannelEntity $salesChannelEntity
  172.      *
  173.      * @return int
  174.      */
  175.     public function getMinimalStockThreshold(SalesChannelEntity $salesChannelEntity): int
  176.     {
  177.         return $this->configService->getMinimalStockThreshold($salesChannelEntity);
  178.     }
  179.     /**
  180.      * @param ProductEntity $product
  181.      * @param SalesChannelEntity $salesChannelEntity
  182.      *
  183.      * @return int
  184.      */
  185.     public function getPreferredProductStock(ProductEntity $productSalesChannelEntity $salesChannelEntity): int
  186.     {
  187.         return $this->configService->getUseAvailableStock($salesChannelEntity)
  188.             ? intval($product->getAvailableStock())
  189.             : $product->getStock();
  190.     }
  191.     /**
  192.      * @param SalesChannelEntity $salesChannelEntity
  193.      *
  194.      * @return bool
  195.      */
  196.     public function getDisableQuantityField(SalesChannelEntity $salesChannelEntity): bool
  197.     {
  198.         return $this->configService->getDisableQuantityField($salesChannelEntity);
  199.     }
  200.     /**
  201.      * @param SalesChannelEntity $salesChannelEntity
  202.      *
  203.      * @return bool
  204.      */
  205.     public function getEnableRecaptcha(SalesChannelEntity $salesChannelEntity): bool
  206.     {
  207.         return $this->configService->getEnableRecaptcha($salesChannelEntity->getId());
  208.     }
  209.     /**
  210.      * @param SalesChannelEntity $salesChannelEntity
  211.      *
  212.      * @return bool
  213.      */
  214.     public function getShowPrivacyStatement(SalesChannelEntity $salesChannelEntity): bool
  215.     {
  216.         return $this->configService->getShowPrivacyStatement($salesChannelEntity->getId());
  217.     }
  218.     /**
  219.      * @param ProductEntity $productEntity
  220.      * @param SalesChannelEntity $salesChannelEntity
  221.      *
  222.      * @return bool
  223.      */
  224.     public function canShowFormForProductOnCloseout(ProductEntity $productEntitySalesChannelEntity $salesChannelEntity): bool
  225.     {
  226.         switch ($this->configService->getBackordersDeniedPresentation($salesChannelEntity)) {
  227.             case ConfigService::BACKORDERS_DENIED_PRESENTATION_ALWAYS:
  228.                 return true;
  229.             case ConfigService::BACKORDERS_DENIED_PRESENTATION_NEVER:
  230.                 return !$productEntity->getIsCloseout();
  231.             case ConfigService::BACKORDERS_DENIED_PRESENTATION_EXCLUSIVE:
  232.                 return $productEntity->getIsCloseout();
  233.         }
  234.         // FALLBACK ON OLD CONFIGURATIONS
  235.         return !$productEntity->getIsCloseout() || !$this->configService->getHideStockSubscriptionFormOnClearanceSale($salesChannelEntity);
  236.     }
  237.     /**
  238.      * @param string $email
  239.      * @param array $products
  240.      * @param string $salesChannelDomainId
  241.      * @param SalesChannelContext $salesChannelContext
  242.      * @param CustomerEntity|null $customer
  243.      *
  244.      * @return string
  245.      */
  246.     public function subscribeToProducts(
  247.         string              $email,
  248.         array               $products,
  249.         string              $salesChannelDomainId,
  250.         SalesChannelContext $salesChannelContext,
  251.         CustomerEntity      $customer null
  252.     ): string
  253.     {
  254.         $newSubscription false;
  255.         $stockSubscriber $this->stockSubscriberRepository->firstByEmail($email$salesChannelContext);
  256.         if ($stockSubscriber instanceof StockSubscriber) {
  257.             $isNew false;
  258.             $id $stockSubscriber->getId();
  259.             $data['customerId'] = !empty($customer) ? $customer->getId() : null;
  260.             if ($stockSubscriber->getProducts()) {
  261.                 $products $this->mergeStockSubscriberProductsWithProducts($stockSubscriber->getProducts(), $products);
  262.             }
  263.         } else {
  264.             $isNew true;
  265.             $id Uuid::randomHex();
  266.             $data['email'] = $email;
  267.             $data['customerId'] = !empty($customer) ? $customer->getId() : null;
  268.             $data['salesChannelId'] = $salesChannelContext->getSalesChannelId();
  269.             $data['salesChannelDomainId'] = $salesChannelDomainId;
  270.             $data['languageIdChain'] = $salesChannelContext->getLanguageIdChain();
  271.         }
  272.         $products array_map(function ($product) use ($id, &$newSubscription) {
  273.             $product['stockSubscriberId'] = $id;
  274.             if (!array_key_exists('id'$product)) {
  275.                 $newSubscription true;
  276.                 $product['id'] = Uuid::randomHex();
  277.             }
  278.             return $product;
  279.         }, $products);
  280.         $data['id'] = $id;
  281.         $data['products'] = array_values($products);
  282.         $this->stockSubscriberRepository->upsert([$data], $salesChannelContext->getContext());
  283.         if ($isNew && $this->configService->getDoubleOptInEnabled($salesChannelContext->getSalesChannel())) {
  284.             $this->messageBus->dispatch(new DoubleOptInMessage($id));
  285.         }
  286.         return $newSubscription self::ACTION_SUBSCRIPTION_CREATED self::ACTION_SUBSCRIPTION_UPDATED;
  287.     }
  288.     /**
  289.      * @param StockSubscriberProduct $stockSubscriberProduct
  290.      *
  291.      * @return bool
  292.      */
  293.     public function shouldNotifySubscriber(StockSubscriberProduct $stockSubscriberProduct): bool
  294.     {
  295.         $product $stockSubscriberProduct->getProduct();
  296.         $salesChannel $stockSubscriberProduct->getStockSubscriber()->getSalesChannel();
  297.         $subscriber $stockSubscriberProduct->getStockSubscriber();
  298.         if ($this->doubleOptInEnabled($salesChannel) && !$subscriber->getDoubleOptInAccepted()) {
  299.             return false;
  300.         }
  301.         # For backwards compatibility, if e-mail is invalid we mark the subscription as notified
  302.         if (filter_var($subscriber->getEmail(), FILTER_VALIDATE_EMAIL) === false) {
  303.             $this->markNotified($stockSubscriberProductContext::createDefaultContext());
  304.             return false;
  305.         }
  306.         $minStockThreshold $product->getCustomFields()['zeobvGetNotifiedMinStockToNotify'] ?? 0;
  307.         return $this->getPreferredProductStock($product$salesChannel) >= $stockSubscriberProduct->getQuantity()
  308.             && $this->getPreferredProductStock($product$salesChannel) >= $this->getMinimalStockThreshold($salesChannel)
  309.             && $this->getPreferredProductStock($product$salesChannel) >= $minStockThreshold;
  310.     }
  311.     /**
  312.      * @param StockSubscriberProduct $stockSubscriberProduct
  313.      * @param Context $context
  314.      * @param string|null $mailTemplateId
  315.      *
  316.      * @return bool
  317.      */
  318.     public function notifySubscriber(StockSubscriberProduct $stockSubscriberProductContext $context, ?string $mailTemplateId null): bool
  319.     {
  320.         if (!$this->shouldNotifySubscriber($stockSubscriberProduct)) {
  321.             return false;
  322.         }
  323.         $product $stockSubscriberProduct->getProduct();
  324.         $languageId $stockSubscriberProduct->getStockSubscriber()->getLanguageIdChain()[0];
  325.         if ($product !== null) {
  326.             $customFields $product->getCustomFields();
  327.             if ($customFields !== null && $customFields !== []) {
  328.                 // Save number of times notifications were sent for this product to a custom field
  329.                 $notificationsSent key_exists('zeobvGetNotifiedNotificationsSent'$customFields)
  330.                     ? intval($customFields['zeobvGetNotifiedNotificationsSent']) + 1
  331.                     1;
  332.             } else {
  333.                 $notificationsSent 1;
  334.                 $sql "UPDATE `product_translation`
  335.                 SET `custom_fields` = '{}'
  336.                 WHERE `product_id` = :productId
  337.                 AND `language_id` = :languageId";
  338.                 $result $this->connection->executeStatement($sql, [
  339.                     'productId' => Uuid::fromHexToBytes($product->getId()),
  340.                     'languageId' => Uuid::fromHexToBytes($languageId)
  341.                 ]);
  342.             }
  343.             $sql "UPDATE `product_translation`
  344.                 SET `custom_fields` = JSON_SET(custom_fields, '$.zeobvGetNotifiedNotificationsSent', :notificationsSent)
  345.                 WHERE `product_id` = :productId
  346.                 AND `language_id` = :languageId";
  347.             $result $this->connection->executeStatement($sql, [
  348.                 'notificationsSent' => $notificationsSent,
  349.                 'productId' => Uuid::fromHexToBytes($product->getId()),
  350.                 'languageId' => Uuid::fromHexToBytes($languageId)
  351.             ]);
  352.         }
  353.         $result $this->mailService->sendBackInStockNotification($stockSubscriberProduct$context$mailTemplateId);
  354.         if (empty($result)) {
  355.             return false;
  356.         }
  357.         $entityWrittenEvent $this->markNotified($stockSubscriberProduct$context);
  358.         return empty($entityWrittenEvent->getErrors());
  359.     }
  360.     /**
  361.      * @param StockSubscriberProduct $stockSubscriberProduct
  362.      * @param Context $context
  363.      *
  364.      * @return EntityWrittenContainerEvent
  365.      */
  366.     public function markNotified(StockSubscriberProduct $stockSubscriberProductContext $context): EntityWrittenContainerEvent
  367.     {
  368.         $entityWrittenEvent $this->stockSubscriberRepository->update([
  369.             [
  370.                 'id' => $stockSubscriberProduct->getStockSubscriber()->getId(),
  371.                 'products' => [[
  372.                     'id' => $stockSubscriberProduct->getId(),
  373.                     'notified' => true,
  374.                 ]]
  375.             ]
  376.         ], $context);
  377.         if (!empty($entityWrittenEvent->getErrors())) {
  378.             return $entityWrittenEvent;
  379.         }
  380.         if (!$this->configService->getAutoRemoveOnNotified(
  381.             $stockSubscriberProduct->getStockSubscriber()->getSalesChannel()
  382.         )) {
  383.             return $entityWrittenEvent;
  384.         }
  385.         /** @var StockSubscriber $stockSubscriber */
  386.         $stockSubscriber $this->stockSubscriberRepository->search(
  387.             (new Criteria([$stockSubscriberProduct->getStockSubscriber()->getId()]))->addAssociation('products'),
  388.             $context
  389.         )->first();
  390.         $notifiedProducts $stockSubscriber->getProducts()->filterByProperty('notified'true);
  391.         # Check if all notifications have been sent for subscribed products and remove the subscriber if so
  392.         if ($stockSubscriber->getProducts()->count() === $notifiedProducts->count()) {
  393.             $this->stockSubscriberProductRepository->delete(array_values(array_map(function ($id) {
  394.                 return ['id' => $id];
  395.             }, $notifiedProducts->getIds())), $context);
  396.             $this->stockSubscriberRepository->delete([
  397.                 ['id' => $stockSubscriber->getId()]
  398.             ], $context);
  399.         } else {
  400.             # Remove the notified products from the subscriber
  401.             $this->stockSubscriberProductRepository->delete(array_values(array_map(function ($id) {
  402.                 return ['id' => $id];
  403.             }, $notifiedProducts->getIds())), $context);
  404.         }
  405.         return $entityWrittenEvent;
  406.     }
  407.     /**
  408.      * @param StockSubscriberProductCollection $stockSubscriberProductCollection
  409.      * @param array $products
  410.      *
  411.      * @return array
  412.      */
  413.     protected function mergeStockSubscriberProductsWithProducts(
  414.         StockSubscriberProductCollection $stockSubscriberProductCollection,
  415.         array                            $products
  416.     ): array
  417.     {
  418.         foreach ($stockSubscriberProductCollection->getElements() as $stockSubscriberProduct) {
  419.             # If product id is being resubscribed to add the product ID and set notified to false
  420.             if (array_key_exists($stockSubscriberProduct->getProductId(), $products)) {
  421.                 $products[$stockSubscriberProduct->getProductId()]['id'] = $stockSubscriberProduct->getId();
  422.                 $products[$stockSubscriberProduct->getProductId()]['notified'] = false;
  423.             } else {
  424.                 # else just add the product to the list for update
  425.                 $products[$stockSubscriberProduct->getProductId()] = [
  426.                     'id' => $stockSubscriberProduct->getId(),
  427.                     'productId' => $stockSubscriberProduct->getProductId(),
  428.                     'quantity' => $stockSubscriberProduct->getQuantity(),
  429.                     'notified' => $stockSubscriberProduct->isNotified()
  430.                 ];
  431.             }
  432.         }
  433.         return $products;
  434.     }
  435. }