src/Core/Checkout/Promotion/DataAbstractionLayer/PromotionRedemptionUpdater.php line 92

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Checkout\Promotion\DataAbstractionLayer;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Core\Checkout\Cart\Event\CheckoutOrderPlacedEvent;
  5. use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity;
  6. use Shopware\Core\Checkout\Promotion\Cart\PromotionProcessor;
  7. use Shopware\Core\Defaults;
  8. use Shopware\Core\Framework\Context;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableQuery;
  10. use Shopware\Core\Framework\Log\Package;
  11. use Shopware\Core\Framework\Uuid\Uuid;
  12. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  13. /**
  14.  * @internal
  15.  */
  16. #[Package('checkout')]
  17. class PromotionRedemptionUpdater implements EventSubscriberInterface
  18. {
  19.     /**
  20.      * @internal
  21.      */
  22.     public function __construct(private readonly Connection $connection)
  23.     {
  24.     }
  25.     /**
  26.      * @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
  27.      */
  28.     public static function getSubscribedEvents(): array
  29.     {
  30.         return [
  31.             CheckoutOrderPlacedEvent::class => 'orderPlaced',
  32.         ];
  33.     }
  34.     /**
  35.      * @param array<string> $ids
  36.      */
  37.     public function update(array $idsContext $context): void
  38.     {
  39.         $ids array_unique(array_filter($ids));
  40.         if (empty($ids) || $context->getVersionId() !== Defaults::LIVE_VERSION) {
  41.             return;
  42.         }
  43.         $sql = <<<'SQL'
  44.                 SELECT LOWER(HEX(order_line_item.promotion_id)) as promotion_id,
  45.                        COUNT(DISTINCT order_line_item.id) as total,
  46.                        LOWER(HEX(order_customer.customer_id)) as customer_id
  47.                 FROM order_line_item
  48.                 INNER JOIN order_customer
  49.                     ON order_customer.order_id = order_line_item.order_id
  50.                     AND order_customer.version_id = order_line_item.version_id
  51.                 WHERE order_line_item.type = :type
  52.                 AND order_line_item.promotion_id IN (:ids)
  53.                 AND order_line_item.version_id = :versionId
  54.                 GROUP BY order_line_item.promotion_id, order_customer.customer_id
  55. SQL;
  56.         $promotions $this->connection->fetchAllAssociative(
  57.             $sql,
  58.             ['type' => PromotionProcessor::LINE_ITEM_TYPE'ids' => Uuid::fromHexToBytesList($ids), 'versionId' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION)],
  59.             ['ids' => Connection::PARAM_STR_ARRAY]
  60.         );
  61.         if (empty($promotions)) {
  62.             return;
  63.         }
  64.         $update = new RetryableQuery(
  65.             $this->connection,
  66.             $this->connection->prepare('UPDATE promotion SET order_count = :count, orders_per_customer_count = :customerCount WHERE id = :id')
  67.         );
  68.         // group the promotions to update each promotion with a single update statement
  69.         $promotions $this->groupByPromotion($promotions);
  70.         foreach ($promotions as $id => $totals) {
  71.             $total array_sum($totals);
  72.             $update->execute([
  73.                 'id' => Uuid::fromHexToBytes($id),
  74.                 'count' => (int) $total,
  75.                 'customerCount' => json_encode($totals\JSON_THROW_ON_ERROR),
  76.             ]);
  77.         }
  78.     }
  79.     public function orderPlaced(CheckoutOrderPlacedEvent $event): void
  80.     {
  81.         $lineItems $event->getOrder()->getLineItems();
  82.         $customer $event->getOrder()->getOrderCustomer();
  83.         if (!$lineItems || !$customer) {
  84.             return;
  85.         }
  86.         $promotionIds = [];
  87.         /** @var OrderLineItemEntity $lineItem */
  88.         foreach ($lineItems as $lineItem) {
  89.             if ($lineItem->getType() !== PromotionProcessor::LINE_ITEM_TYPE) {
  90.                 continue;
  91.             }
  92.             $promotionId $lineItem->getPromotionId();
  93.             if ($promotionId === null) {
  94.                 continue;
  95.             }
  96.             $promotionIds[] = $promotionId;
  97.         }
  98.         if (!$promotionIds) {
  99.             return;
  100.         }
  101.         $allCustomerCounts $this->getAllCustomerCounts($promotionIds);
  102.         $update = new RetryableQuery(
  103.             $this->connection,
  104.             $this->connection->prepare('UPDATE promotion SET order_count = order_count + 1, orders_per_customer_count = :customerCount WHERE id = :id')
  105.         );
  106.         foreach ($promotionIds as $promotionId) {
  107.             $customerId $customer->getCustomerId();
  108.             if ($customerId !== null) {
  109.                 $allCustomerCounts[$promotionId][$customerId] = + ($allCustomerCounts[$promotionId][$customerId] ?? 0);
  110.             }
  111.             $update->execute([
  112.                 'id' => Uuid::fromHexToBytes($promotionId),
  113.                 'customerCount' => json_encode($allCustomerCounts[$promotionId], \JSON_THROW_ON_ERROR),
  114.             ]);
  115.         }
  116.     }
  117.     /**
  118.      * @param array<mixed> $promotions
  119.      *
  120.      * @return array<mixed>
  121.      */
  122.     private function groupByPromotion(array $promotions): array
  123.     {
  124.         $grouped = [];
  125.         foreach ($promotions as $promotion) {
  126.             $id $promotion['promotion_id'];
  127.             $customerId $promotion['customer_id'];
  128.             $grouped[$id][$customerId] = (int) $promotion['total'];
  129.         }
  130.         return $grouped;
  131.     }
  132.     /**
  133.      * @param array<string> $promotionIds
  134.      *
  135.      * @return array<string>
  136.      */
  137.     private function getAllCustomerCounts(array $promotionIds): array
  138.     {
  139.         $allCustomerCounts = [];
  140.         $countResult $this->connection->fetchAllAssociative(
  141.             'SELECT `id`, `orders_per_customer_count` FROM `promotion` WHERE `id` IN (:ids)',
  142.             ['ids' => Uuid::fromHexToBytesList($promotionIds)],
  143.             ['ids' => Connection::PARAM_STR_ARRAY]
  144.         );
  145.         foreach ($countResult as $row) {
  146.             if (!\is_string($row['orders_per_customer_count'])) {
  147.                 $allCustomerCounts[Uuid::fromBytesToHex($row['id'])] = [];
  148.                 continue;
  149.             }
  150.             $customerCount json_decode($row['orders_per_customer_count'], true512\JSON_THROW_ON_ERROR);
  151.             if (!$customerCount) {
  152.                 $allCustomerCounts[Uuid::fromBytesToHex($row['id'])] = [];
  153.                 continue;
  154.             }
  155.             $allCustomerCounts[Uuid::fromBytesToHex($row['id'])] = $customerCount;
  156.         }
  157.         return $allCustomerCounts;
  158.     }
  159. }