custom/plugins/AcrisStockCS/src/Core/Content/Product/DataAbstractionLayer/StockUpdater.php line 76

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Acris\Stock\Core\Content\Product\DataAbstractionLayer;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Core\Checkout\Cart\Event\CheckoutOrderPlacedEvent;
  5. use Shopware\Core\Checkout\Cart\LineItem\LineItem;
  6. use Shopware\Core\Checkout\Order\OrderStates;
  7. use Shopware\Core\Content\Product\Events\ProductNoLongerAvailableEvent;
  8. use Shopware\Core\Defaults;
  9. use Shopware\Core\Framework\Context;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableQuery;
  11. use Shopware\Core\Framework\DataAbstractionLayer\EntityWriteResult;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent;
  14. use Shopware\Core\Framework\Uuid\Uuid;
  15. use Shopware\Core\System\StateMachine\Event\StateMachineTransitionEvent;
  16. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  17. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  18. use Shopware\Core\System\SystemConfig\SystemConfigService;
  19. class StockUpdater extends \Shopware\Core\Content\Product\DataAbstractionLayer\StockUpdater
  20. {
  21.     private Connection $connection;
  22.     private EventDispatcherInterface $dispatcher;
  23.     private EventSubscriberInterface $parent;
  24.     private SystemConfigService $systemConfigService;
  25.     public function __construct(
  26.         Connection               $connection,
  27.         EventDispatcherInterface $dispatcher,
  28.         SystemConfigService $systemConfigService,
  29.         EventSubscriberInterface $parent
  30.     )
  31.     {
  32.         $this->connection $connection;
  33.         $this->dispatcher $dispatcher;
  34.         $this->systemConfigService $systemConfigService;
  35.         $this->parent $parent;
  36.     }
  37.     /**
  38.      * Returns a list of custom business events to listen where the product maybe changed
  39.      *
  40.      * @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
  41.      */
  42.     public static function getSubscribedEvents()
  43.     {
  44.         return parent::getSubscribedEvents();
  45.     }
  46.     public function triggerChangeSet(PreWriteValidationEvent $event): void
  47.     {
  48.         $this->parent->triggerChangeSet($event);
  49.     }
  50.     public function orderPlaced(CheckoutOrderPlacedEvent $event): void
  51.     {
  52.         $ids = [];
  53.         foreach ($event->getOrder()->getLineItems() as $lineItem) {
  54.             if ($lineItem->getType() !== LineItem::PRODUCT_LINE_ITEM_TYPE) {
  55.                 continue;
  56.             }
  57.             $ids[] = $lineItem->getReferencedId();
  58.         }
  59.         $this->update($ids$event->getContext());
  60.     }
  61.     /**
  62.      * If the product of an order item changed, the stocks of the old product and the new product must be updated.
  63.      */
  64.     public function lineItemWritten(EntityWrittenEvent $event): void
  65.     {
  66.         $ids = [];
  67.         // we don't want to trigger to `update` method when we are inside the order process
  68.         if ($event->getContext()->hasState('checkout-order-route')) {
  69.             return;
  70.         }
  71.         foreach ($event->getWriteResults() as $result) {
  72.             if ($result->hasPayload('referencedId') && $result->getProperty('type') === LineItem::PRODUCT_LINE_ITEM_TYPE) {
  73.                 $ids[] = $result->getProperty('referencedId');
  74.             }
  75.             if ($result->getOperation() === EntityWriteResult::OPERATION_INSERT) {
  76.                 continue;
  77.             }
  78.             $changeSet $result->getChangeSet();
  79.             if (!$changeSet) {
  80.                 continue;
  81.             }
  82.             $type $changeSet->getBefore('type');
  83.             if ($type !== LineItem::PRODUCT_LINE_ITEM_TYPE) {
  84.                 continue;
  85.             }
  86.             if (!$changeSet->hasChanged('referenced_id') && !$changeSet->hasChanged('quantity')) {
  87.                 continue;
  88.             }
  89.             $ids[] = $changeSet->getBefore('referenced_id');
  90.             $ids[] = $changeSet->getAfter('referenced_id');
  91.         }
  92.         $ids array_filter(array_unique($ids));
  93.         if (empty($ids)) {
  94.             return;
  95.         }
  96.         $this->update($ids$event->getContext());
  97.     }
  98.     public function stateChanged(StateMachineTransitionEvent $event): void
  99.     {
  100.         if ($event->getContext()->getVersionId() !== Defaults::LIVE_VERSION) {
  101.             return;
  102.         }
  103.         if ($event->getEntityName() !== 'order') {
  104.             return;
  105.         }
  106.         if ($event->getToPlace()->getTechnicalName() === OrderStates::STATE_IN_PROGRESS) {
  107.             $this->decreaseStock($event);
  108.             return;
  109.         }
  110.         if (($event->getToPlace()->getTechnicalName() === OrderStates::STATE_OPEN && $event->getFromPlace()->getTechnicalName() !== OrderStates::STATE_CANCELLED && empty($event->getFromPlace()->getTechnicalName()) === false)
  111.             or ($event->getToPlace()->getTechnicalName() === OrderStates::STATE_CANCELLED && $event->getFromPlace()->getTechnicalName() !== OrderStates::STATE_OPEN)) {
  112.             if($event->getToPlace()->getTechnicalName() === OrderStates::STATE_CANCELLED
  113.                 && $this->systemConfigService->get('AcrisStockCS.config.cancelOrderIncreaseCondition') === 'ignore') {
  114.                 $products $this->getProductsOfOrder($event->getEntityId());
  115.                 $ids array_column($products'referenced_id');
  116.                 $this->updateAvailableStockAndSales($ids$event->getContext());
  117.                 $this->updateAvailableFlag($ids$event->getContext());
  118.                 return;
  119.             }
  120.             $this->increaseStock($event);
  121.             return;
  122.         }
  123.         if ($event->getToPlace()->getTechnicalName() === OrderStates::STATE_CANCELLED || $event->getFromPlace()->getTechnicalName() === OrderStates::STATE_CANCELLED) {
  124.             $products $this->getProductsOfOrder($event->getEntityId());
  125.             $ids array_column($products'referenced_id');
  126.             $this->updateAvailableStockAndSales($ids$event->getContext());
  127.             $this->updateAvailableFlag($ids$event->getContext());
  128.         }
  129.     }
  130.     public function update(array $idsContext $context): void
  131.     {
  132.         if ($context->getVersionId() !== Defaults::LIVE_VERSION) {
  133.             return;
  134.         }
  135.         $this->updateAvailableStockAndSales($ids$context);
  136.         $this->updateAvailableFlag($ids$context);
  137.     }
  138.     public function decreaseStockOnActivation(array $productsContext $context)
  139.     {
  140.         $this->adjustStock($products$context, -1);
  141.     }
  142.     public function increaseStockOnDeactivation(array $productsContext $context)
  143.     {
  144.         $this->adjustStockOriginal($products$context, +1true);
  145.     }
  146.     private function increaseStock(StateMachineTransitionEvent $event): void
  147.     {
  148.         $products $this->getProductsOfOrder($event->getEntityId());
  149.         $this->adjustStock($products$event->getContext(), +1);
  150.     }
  151.     private function decreaseStock(StateMachineTransitionEvent $event): void
  152.     {
  153.         $products $this->getProductsOfOrder($event->getEntityId());
  154.         $this->adjustStock($products$event->getContext(), -1);
  155.     }
  156.     private function adjustStock(array $productsContext $contextint $multiplier)
  157.     {
  158.         $ids array_column($products'referenced_id');
  159.         $this->updateStock($products$multiplier);
  160.         $this->updateAvailableStockAndSales($ids$context);
  161.         $this->updateAvailableFlag($ids$context);
  162.     }
  163.     private function adjustStockOriginal(array $productsContext $contextint $multiplierbool $skipUpdateAvailableStockAndSales false)
  164.     {
  165.         $ids array_column($products'referenced_id');
  166.         $this->updateStock($products$multiplier);
  167.         if($skipUpdateAvailableStockAndSales !== true) {
  168.             $this->updateAvailableStockAndSalesOriginal($ids$context);
  169.         }
  170.         $this->updateAvailableFlag($ids$context);
  171.     }
  172.     private function updateAvailableStockAndSales(array $idsContext $context): void
  173.     {
  174.         $ids array_filter(array_keys(array_flip($ids)));
  175.         if (empty($ids)) {
  176.             return;
  177.         }
  178.         $sql '
  179. SELECT LOWER(HEX(order_line_item.product_id)) as product_id,
  180.     IFNULL(
  181.         SUM(IF(state_machine_state.technical_name = :open_state, IF(order_delivery_position.quantity IS NOT NULL, order_delivery_position.quantity, order_line_item.quantity), 0)),
  182.         0
  183.     ) as open_quantity,
  184.     IFNULL(
  185.         SUM(IF(state_machine_state.technical_name = :completed_state, IF(order_delivery_position.quantity IS NOT NULL, order_delivery_position.quantity, order_line_item.quantity), 0)),
  186.         0
  187.     ) as sales_quantity
  188. FROM order_line_item
  189.     INNER JOIN `order`
  190.         ON `order`.id = order_line_item.order_id
  191.         AND `order`.version_id = order_line_item.order_version_id
  192.     INNER JOIN state_machine_state
  193.         ON state_machine_state.id = `order`.state_id
  194.         AND (state_machine_state.technical_name = :completed_state OR state_machine_state.technical_name = :open_state)
  195.     LEFT JOIN `order_delivery_position`
  196.         ON `order_delivery_position`.order_line_item_id = order_line_item.id
  197.         AND `order_delivery_position`.order_line_item_version_id = `order_line_item`.version_id
  198. WHERE order_line_item.product_id IN (:ids)
  199.     AND order_line_item.type = :type
  200.     AND order_line_item.version_id = :version
  201.     AND order_line_item.product_id IS NOT NULL
  202. GROUP BY product_id;
  203.         ';
  204.         $rows $this->connection->fetchAllAssociative(
  205.             $sql,
  206.             [
  207.                 'type' => LineItem::PRODUCT_LINE_ITEM_TYPE,
  208.                 'version' => Uuid::fromHexToBytes($context->getVersionId()),
  209.                 'completed_state' => OrderStates::STATE_COMPLETED,
  210.                 'open_state' => OrderStates::STATE_OPEN,
  211.                 'ids' => Uuid::fromHexToBytesList($ids),
  212.             ],
  213.             [
  214.                 'ids' => Connection::PARAM_STR_ARRAY,
  215.             ]
  216.         );
  217.         $fallback array_column($rows'product_id');
  218.         $fallback array_diff($ids$fallback);
  219.         $update = new RetryableQuery(
  220.             $this->connection,
  221.             $this->connection->prepare('UPDATE product SET available_stock = stock - :open_quantity, sales = :sales_quantity, updated_at = :now WHERE id = :id')
  222.         );
  223.         foreach ($fallback as $id) {
  224.             $update->execute([
  225.                 'id' => Uuid::fromHexToBytes((string)$id),
  226.                 'open_quantity' => 0,
  227.                 'sales_quantity' => 0,
  228.                 'now' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
  229.             ]);
  230.         }
  231.         foreach ($rows as $row) {
  232.             $update->execute([
  233.                 'id' => Uuid::fromHexToBytes($row['product_id']),
  234.                 'open_quantity' => $row['open_quantity'],
  235.                 'sales_quantity' => $row['sales_quantity'],
  236.                 'now' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
  237.             ]);
  238.         }
  239.     }
  240.     private function updateAvailableStockAndSalesOriginal(array $idsContext $context): void
  241.     {
  242.         $ids array_filter(array_keys(array_flip($ids)));
  243.         if (empty($ids)) {
  244.             return;
  245.         }
  246.         $sql '
  247. SELECT LOWER(HEX(order_line_item.product_id)) as product_id,
  248.     IFNULL(
  249.         SUM(IF(state_machine_state.technical_name = :completed_state, 0, IF(order_delivery_position.quantity IS NOT NULL, order_delivery_position.quantity, order_line_item.quantity))),
  250.         0
  251.     ) as open_quantity,
  252.     IFNULL(
  253.         SUM(IF(state_machine_state.technical_name = :completed_state, IF(order_delivery_position.quantity IS NOT NULL, order_delivery_position.quantity, order_line_item.quantity), 0)),
  254.         0
  255.     ) as sales_quantity
  256. FROM order_line_item
  257.     INNER JOIN `order`
  258.         ON `order`.id = order_line_item.order_id
  259.         AND `order`.version_id = order_line_item.order_version_id
  260.     INNER JOIN state_machine_state
  261.         ON state_machine_state.id = `order`.state_id
  262.         AND state_machine_state.technical_name <> :cancelled_state
  263.     LEFT JOIN `order_delivery_position`
  264.         ON `order_delivery_position`.order_line_item_id = order_line_item.id
  265.         AND `order_delivery_position`.order_line_item_version_id = `order_line_item`.version_id
  266. WHERE order_line_item.product_id IN (:ids)
  267.     AND order_line_item.type = :type
  268.     AND order_line_item.version_id = :version
  269.     AND order_line_item.product_id IS NOT NULL
  270. GROUP BY product_id;
  271.         ';
  272.         $rows $this->connection->fetchAllAssociative(
  273.             $sql,
  274.             [
  275.                 'type' => LineItem::PRODUCT_LINE_ITEM_TYPE,
  276.                 'version' => Uuid::fromHexToBytes($context->getVersionId()),
  277.                 'completed_state' => OrderStates::STATE_COMPLETED,
  278.                 'cancelled_state' => OrderStates::STATE_CANCELLED,
  279.                 'ids' => Uuid::fromHexToBytesList($ids),
  280.             ],
  281.             [
  282.                 'ids' => Connection::PARAM_STR_ARRAY,
  283.             ]
  284.         );
  285.         $fallback array_column($rows'product_id');
  286.         $fallback array_diff($ids$fallback);
  287.         $update = new RetryableQuery(
  288.             $this->connection,
  289.             $this->connection->prepare('UPDATE product SET available_stock = stock - :open_quantity, sales = :sales_quantity, updated_at = :now WHERE id = :id')
  290.         );
  291.         foreach ($fallback as $id) {
  292.             $update->execute([
  293.                 'id' => Uuid::fromHexToBytes((string)$id),
  294.                 'open_quantity' => 0,
  295.                 'sales_quantity' => 0,
  296.                 'now' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
  297.             ]);
  298.         }
  299.         foreach ($rows as $row) {
  300.             $update->execute([
  301.                 'id' => Uuid::fromHexToBytes($row['product_id']),
  302.                 'open_quantity' => $row['open_quantity'],
  303.                 'sales_quantity' => $row['sales_quantity'],
  304.                 'now' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
  305.             ]);
  306.         }
  307.     }
  308.     private function updateAvailableFlag(array $idsContext $context): void
  309.     {
  310.         $ids array_filter(array_unique($ids));
  311.         if (empty($ids)) {
  312.             return;
  313.         }
  314.         $bytes Uuid::fromHexToBytesList($ids);
  315.         $sql '
  316.             UPDATE product
  317.             LEFT JOIN product parent
  318.                 ON parent.id = product.parent_id
  319.                 AND parent.version_id = product.version_id
  320.             SET product.available = IFNULL((
  321.                 IFNULL(product.is_closeout, parent.is_closeout) * product.available_stock
  322.                 >=
  323.                 IFNULL(product.is_closeout, parent.is_closeout) * IFNULL(product.min_purchase, parent.min_purchase)
  324.             ), 0)
  325.             WHERE product.id IN (:ids)
  326.             AND product.version_id = :version
  327.         ';
  328.         RetryableQuery::retryable($this->connection, function () use ($sql$context$bytes): void {
  329.             $this->connection->executeUpdate(
  330.                 $sql,
  331.                 ['ids' => $bytes'version' => Uuid::fromHexToBytes($context->getVersionId())],
  332.                 ['ids' => Connection::PARAM_STR_ARRAY]
  333.             );
  334.         });
  335.         $updated $this->connection->fetchFirstColumn(
  336.             'SELECT LOWER(HEX(id)) FROM product WHERE available = 0 AND id IN (:ids) AND product.version_id = :version',
  337.             ['ids' => $bytes'version' => Uuid::fromHexToBytes($context->getVersionId())],
  338.             ['ids' => Connection::PARAM_STR_ARRAY]
  339.         );
  340.         if (!empty($updated)) {
  341.             $this->dispatcher->dispatch(new ProductNoLongerAvailableEvent($updated$context));
  342.         }
  343.     }
  344.     private function updateStock(array $productsint $multiplier): void
  345.     {
  346.         $query = new RetryableQuery(
  347.             $this->connection,
  348.             $this->connection->prepare('UPDATE product SET stock = stock + :quantity WHERE id = :id AND version_id = :version')
  349.         );
  350.         foreach ($products as $product) {
  351.             $query->execute([
  352.                 'quantity' => (int)$product['quantity'] * $multiplier,
  353.                 'id' => Uuid::fromHexToBytes($product['referenced_id']),
  354.                 'version' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION),
  355.             ]);
  356.         }
  357.     }
  358.     private function getProductsOfOrder(string $orderId): array
  359.     {
  360.         $query $this->connection->createQueryBuilder();
  361.         $query->select(['referenced_id''quantity']);
  362.         $query->from('order_line_item');
  363.         $query->andWhere('type = :type');
  364.         $query->andWhere('order_id = :id');
  365.         $query->andWhere('version_id = :version');
  366.         $query->setParameter('id'Uuid::fromHexToBytes($orderId));
  367.         $query->setParameter('version'Uuid::fromHexToBytes(Defaults::LIVE_VERSION));
  368.         $query->setParameter('type'LineItem::PRODUCT_LINE_ITEM_TYPE);
  369.         return $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
  370.     }
  371. }