custom/static-plugins/K3nMerchant/src/Subscriber/StateMachine/StateMachineStateChanged.php line 72

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace K3n\Merchant\Subscriber\StateMachine;
  4. use Doctrine\DBAL\Connection;
  5. use Shopware\Core\Checkout\Cart\LineItem\LineItem;
  6. use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemDefinition;
  7. use Shopware\Core\Checkout\Order\OrderStates;
  8. use Shopware\Core\Content\Product\Events\ProductNoLongerAvailableEvent;
  9. use Shopware\Core\Defaults;
  10. use Shopware\Core\Framework\Context;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableQuery;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\ChangeSetAware;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\DeleteCommand;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\InsertCommand;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\UpdateCommand;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent;
  17. use Shopware\Core\Framework\Uuid\Uuid;
  18. use Shopware\Core\System\StateMachine\Event\StateMachineTransitionEvent;
  19. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  20. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  21. class StateMachineStateChanged implements EventSubscriberInterface
  22. {
  23.     private Connection $connection;
  24.     private EventDispatcherInterface $dispatcher;
  25.     public function __construct(
  26.         Connection $connection,
  27.         EventDispatcherInterface $dispatcher
  28.     ) {
  29.         $this->connection $connection;
  30.         $this->dispatcher $dispatcher;
  31.     }
  32.     public static function getSubscribedEvents(): array
  33.     {
  34.         return [
  35.             StateMachineTransitionEvent::class => 'stateChanged',
  36.             PreWriteValidationEvent::class => 'triggerChangeSet',
  37.         ];
  38.     }
  39.     public function triggerChangeSet(PreWriteValidationEvent $event): void
  40.     {
  41.         if ($event->getContext()->getVersionId() !== Defaults::LIVE_VERSION) {
  42.             return;
  43.         }
  44.         foreach ($event->getCommands() as $command) {
  45.             if (!$command instanceof ChangeSetAware) {
  46.                 continue;
  47.             }
  48.             /** @var ChangeSetAware|InsertCommand|UpdateCommand $command */
  49.             if ($command->getDefinition()->getEntityName() !== OrderLineItemDefinition::ENTITY_NAME) {
  50.                 continue;
  51.             }
  52.             if ($command instanceof DeleteCommand) {
  53.                 $command->requestChangeSet();
  54.                 continue;
  55.             }
  56.             if ($command->hasField('referenced_id') || $command->hasField('product_id') || $command->hasField('quantity')) {
  57.                 $command->requestChangeSet();
  58.             }
  59.         }
  60.     }
  61.     public function stateChanged(StateMachineTransitionEvent $event): void
  62.     {
  63.         if ($event->getToPlace()->getTechnicalName() === 'purchase') {
  64.             $this->decreaseStock($event);
  65.             return;
  66.         }
  67.         if ($event->getFromPlace()->getTechnicalName() === 'purchase') {
  68.             $this->increaseStock($event);
  69.             return;
  70.         }
  71.         if ($event->getToPlace()->getTechnicalName() === 'rejected' || $event->getToPlace()->getTechnicalName() === 'cancelled') {
  72.             $products $this->getProductsOfOrder($event->getEntityId());
  73.             $ids array_column($products'referenced_id');
  74.             $this->updateRejectedAvailableStockAndSales($ids$event->getContext());
  75.             $this->updateAvailableFlag($ids$event->getContext());
  76.         }
  77.     }
  78.     private function increaseStock(StateMachineTransitionEvent $event): void
  79.     {
  80.         $products $this->getProductsOfOrder($event->getEntityId());
  81.         $ids array_column($products'referenced_id');
  82.         $this->updateStock($products, +1);
  83.         // $this->updateAvailableStockAndSales($ids, $event->getContext());
  84.         $this->updateAvailableFlag($ids$event->getContext());
  85.     }
  86.     private function decreaseStock(StateMachineTransitionEvent $event): void
  87.     {
  88.         $products $this->getProductsOfOrder($event->getEntityId());
  89.         $ids array_column($products'referenced_id');
  90.         $this->updateStock($products, -1);
  91.         // $this->updateAvailableStockAndSales($ids, $event->getContext());
  92.         $this->updateAvailableFlag($ids$event->getContext());
  93.     }
  94.     private function updateRejectedAvailableStockAndSales(array $idsContext $context): void
  95.     {
  96.         $ids array_filter(array_keys(array_flip($ids)));
  97.         if (empty($ids)) {
  98.             return;
  99.         }
  100.         $sql '
  101. SELECT LOWER(HEX(order_line_item.product_id)) as product_id,
  102.     IFNULL(
  103.         SUM(IF(state_machine_state.technical_name = :completed_state, 0, order_line_item.quantity)),
  104.         0
  105.     ) as open_quantity,
  106.     IFNULL(
  107.         SUM(IF(state_machine_state.technical_name = :completed_state, order_line_item.quantity, 0)),
  108.         0
  109.     ) as sales_quantity
  110. FROM order_line_item
  111.     INNER JOIN `order`
  112.         ON `order`.id = order_line_item.order_id
  113.         AND `order`.version_id = order_line_item.order_version_id
  114.     INNER JOIN state_machine_state
  115.         ON state_machine_state.id = `order`.state_id
  116.         AND state_machine_state.technical_name <> :rejected_state
  117.         AND state_machine_state.technical_name <> :cancelled_state
  118. WHERE order_line_item.product_id IN (:ids)
  119.     AND order_line_item.type = :type
  120.     AND order_line_item.version_id = :version
  121.     AND order_line_item.product_id IS NOT NULL
  122. GROUP BY product_id;
  123.         ';
  124.         $rows $this->connection->fetchAllAssociative(
  125.             $sql,
  126.             [
  127.                 'type' => LineItem::PRODUCT_LINE_ITEM_TYPE,
  128.                 'version' => Uuid::fromHexToBytes($context->getVersionId()),
  129.                 'completed_state' => OrderStates::STATE_COMPLETED,
  130.                 'cancelled_state' => OrderStates::STATE_CANCELLED,
  131.                 'rejected_state' => 'rejected',
  132.                 'ids' => Uuid::fromHexToBytesList($ids),
  133.             ],
  134.             [
  135.                 'ids' => Connection::PARAM_STR_ARRAY,
  136.             ]
  137.         );
  138.         $fallback array_column($rows'product_id');
  139.         $fallback array_diff($ids$fallback);
  140.         $update = new RetryableQuery(
  141.             $this->connection,
  142.             $this->connection->prepare('UPDATE product SET available_stock = stock - :open_quantity, sales = :sales_quantity, updated_at = :now WHERE id = :id')
  143.         );
  144.         foreach ($fallback as $id) {
  145.             $update->execute([
  146.                 'id' => Uuid::fromHexToBytes((string) $id),
  147.                 'open_quantity' => 0,
  148.                 'sales_quantity' => 0,
  149.                 'now' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
  150.             ]);
  151.         }
  152.         foreach ($rows as $row) {
  153.             $update->execute([
  154.                 'id' => Uuid::fromHexToBytes($row['product_id']),
  155.                 'open_quantity' => $row['open_quantity'],
  156.                 'sales_quantity' => $row['sales_quantity'],
  157.                 'now' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
  158.             ]);
  159.         }
  160.     }
  161.     private function updateAvailableStockAndSales(array $idsContext $context): void
  162.     {
  163.         $ids array_filter(array_keys(array_flip($ids)));
  164.         if (empty($ids)) {
  165.             return;
  166.         }
  167.         $sql '
  168. SELECT LOWER(HEX(order_line_item.product_id)) as product_id,
  169.     IFNULL(
  170.         SUM(IF(state_machine_state.technical_name = :completed_state, 0, order_line_item.quantity)),
  171.         0
  172.     ) as open_quantity,
  173.     IFNULL(
  174.         SUM(IF(state_machine_state.technical_name = :completed_state, order_line_item.quantity, 0)),
  175.         0
  176.     ) as sales_quantity
  177. FROM order_line_item
  178.     INNER JOIN `order`
  179.         ON `order`.id = order_line_item.order_id
  180.         AND `order`.version_id = order_line_item.order_version_id
  181.     INNER JOIN state_machine_state
  182.         ON state_machine_state.id = `order`.state_id
  183.         AND state_machine_state.technical_name <> :cancelled_state
  184. WHERE order_line_item.product_id IN (:ids)
  185.     AND order_line_item.type = :type
  186.     AND order_line_item.version_id = :version
  187.     AND order_line_item.product_id IS NOT NULL
  188. GROUP BY product_id;
  189.         ';
  190.         $rows $this->connection->fetchAllAssociative(
  191.             $sql,
  192.             [
  193.                 'type' => LineItem::PRODUCT_LINE_ITEM_TYPE,
  194.                 'version' => Uuid::fromHexToBytes($context->getVersionId()),
  195.                 'completed_state' => OrderStates::STATE_COMPLETED,
  196.                 'cancelled_state' => OrderStates::STATE_CANCELLED,
  197.                 'ids' => Uuid::fromHexToBytesList($ids),
  198.             ],
  199.             [
  200.                 'ids' => Connection::PARAM_STR_ARRAY,
  201.             ]
  202.         );
  203.         $fallback array_column($rows'product_id');
  204.         $fallback array_diff($ids$fallback);
  205.         $update = new RetryableQuery(
  206.             $this->connection,
  207.             $this->connection->prepare('UPDATE product SET available_stock = stock - :open_quantity, sales = :sales_quantity, updated_at = :now WHERE id = :id')
  208.         );
  209.         foreach ($fallback as $id) {
  210.             $update->execute([
  211.                 'id' => Uuid::fromHexToBytes((string) $id),
  212.                 'open_quantity' => 0,
  213.                 'sales_quantity' => 0,
  214.                 'now' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
  215.             ]);
  216.         }
  217.         foreach ($rows as $row) {
  218.             $update->execute([
  219.                 'id' => Uuid::fromHexToBytes($row['product_id']),
  220.                 'open_quantity' => $row['open_quantity'],
  221.                 'sales_quantity' => $row['sales_quantity'],
  222.                 'now' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
  223.             ]);
  224.         }
  225.     }
  226.     private function updateAvailableFlag(array $idsContext $context): void
  227.     {
  228.         $ids array_filter(array_unique($ids));
  229.         if (empty($ids)) {
  230.             return;
  231.         }
  232.         $bytes Uuid::fromHexToBytesList($ids);
  233.         $sql '
  234.             UPDATE product
  235.             LEFT JOIN product parent
  236.                 ON parent.id = product.parent_id
  237.                 AND parent.version_id = product.version_id
  238.             SET product.available = IFNULL((
  239.                 IFNULL(product.is_closeout, parent.is_closeout) * product.available_stock
  240.                 >=
  241.                 IFNULL(product.is_closeout, parent.is_closeout) * IFNULL(product.min_purchase, parent.min_purchase)
  242.             ), 0)
  243.             WHERE product.id IN (:ids)
  244.             AND product.version_id = :version
  245.         ';
  246.         RetryableQuery::retryable($this->connection, function () use ($sql$context$bytes): void {
  247.             $this->connection->executeUpdate(
  248.                 $sql,
  249.                 ['ids' => $bytes'version' => Uuid::fromHexToBytes($context->getVersionId())],
  250.                 ['ids' => Connection::PARAM_STR_ARRAY]
  251.             );
  252.         });
  253.         $updated $this->connection->fetchFirstColumn(
  254.             'SELECT LOWER(HEX(id)) FROM product WHERE available = 0 AND id IN (:ids) AND product.version_id = :version',
  255.             ['ids' => $bytes'version' => Uuid::fromHexToBytes($context->getVersionId())],
  256.             ['ids' => Connection::PARAM_STR_ARRAY]
  257.         );
  258.         if (!empty($updated)) {
  259.             $this->dispatcher->dispatch(new ProductNoLongerAvailableEvent($updated$context));
  260.         }
  261.     }
  262.     private function updateStock(array $productsint $multiplier): void
  263.     {
  264.         $query = new RetryableQuery(
  265.             $this->connection,
  266.             $this->connection->prepare('UPDATE product SET stock = stock + :quantity WHERE id = :id AND version_id = :version')
  267.         );
  268.         foreach ($products as $product) {
  269.             $query->execute([
  270.                 'quantity' => (int) $product['quantity'] * $multiplier,
  271.                 'id' => Uuid::fromHexToBytes($product['referenced_id']),
  272.                 'version' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION),
  273.             ]);
  274.         }
  275.     }
  276.     private function getProductsOfOrder(string $orderId): array
  277.     {
  278.         $query $this->connection->createQueryBuilder();
  279.         $query->select(['referenced_id''quantity']);
  280.         $query->from('order_line_item');
  281.         $query->andWhere('type = :type');
  282.         $query->andWhere('order_id = :id');
  283.         $query->andWhere('version_id = :version');
  284.         $query->setParameter('id'Uuid::fromHexToBytes($orderId));
  285.         $query->setParameter('version'Uuid::fromHexToBytes(Defaults::LIVE_VERSION));
  286.         $query->setParameter('type'LineItem::PRODUCT_LINE_ITEM_TYPE);
  287.         return $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
  288.     }
  289. }