custom/plugins/PickwareErpStarter/vendor/pickware/shopware-extensions-bundle/src/OrderConfiguration/OrderConfigurationUpdater.php line 111

Open in your IDE?
  1. <?php
  2. /*
  3.  * Copyright (c) Pickware GmbH. All rights reserved.
  4.  * This file is part of software that is released under a proprietary license.
  5.  * You must not copy, modify, distribute, make publicly available, or execute
  6.  * its contents or parts thereof without express permission by the copyright
  7.  * holder, unless otherwise permitted by law.
  8.  */
  9. declare(strict_types=1);
  10. namespace Pickware\ShopwareExtensionsBundle\OrderConfiguration;
  11. use Doctrine\DBAL\Connection;
  12. use Pickware\DalBundle\RetryableTransaction;
  13. use Pickware\DalBundle\Sql\SqlUuid;
  14. use Pickware\ShopwareExtensionsBundle\OrderTransaction\OrderTransactionCollectionExtension;
  15. use Shopware\Core\Checkout\Order\OrderEvents;
  16. use Shopware\Core\Defaults;
  17. use Shopware\Core\Framework\Context;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
  19. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  20. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  21. /**
  22.  * @deprecated next-major will be removed
  23.  */
  24. class OrderConfigurationUpdater implements EventSubscriberInterface
  25. {
  26.     // Since this update does not rely on any other subscriber, it has a high priority, so it is executed before other
  27.     // subscribers with default priority.
  28.     private const PRIORITY 10;
  29.     private Connection $connection;
  30.     private ?EventDispatcherInterface $eventDispatcher;
  31.     /**
  32.      * Env variable from the container to skip this update (entity creation and updates) all together. We add this to
  33.      * avoid the risk of performance issues or deadlocks if and only if we know for sure (in a controlled environment)
  34.      * that this deprecated OrderConfigurationUpdater is not used.
  35.      */
  36.     private bool $skip;
  37.     /**
  38.      * @deprecated next major version: Second argument "EventDispatcherInterface $eventDispatcher" will be non-optional
  39.      */
  40.     public function __construct(
  41.         Connection $connection,
  42.         ?EventDispatcherInterface $eventDispatcher null,
  43.         $skip null
  44.     ) {
  45.         $this->connection $connection;
  46.         $this->eventDispatcher $eventDispatcher;
  47.         $this->skip = (bool) $skip;
  48.     }
  49.     public static function getSubscribedEvents(): array
  50.     {
  51.         $subscribedFunctions = [
  52.             OrderEvents::ORDER_WRITTEN_EVENT => 'orderWritten',
  53.             OrderEvents::ORDER_DELIVERY_WRITTEN_EVENT => 'orderDeliveryWritten',
  54.             OrderEvents::ORDER_DELIVERY_DELETED_EVENT => 'updateOrderConfigurationAfterDeliveryOrTransactionDeletion',
  55.             OrderEvents::ORDER_TRANSACTION_WRITTEN_EVENT => 'orderTransactionWritten',
  56.             OrderEvents::ORDER_TRANSACTION_DELETED_EVENT => 'updateOrderConfigurationAfterDeliveryOrTransactionDeletion',
  57.         ];
  58.         // All subscribes functions should receive the same priority
  59.         $result = [];
  60.         foreach ($subscribedFunctions as $eventName => $functionName) {
  61.             $result[$eventName] = [
  62.                 $functionName,
  63.                 self::PRIORITY,
  64.             ];
  65.         }
  66.         return $result;
  67.     }
  68.     /**
  69.      * This subscriber method only ensures that the order configuration exists (without updating the actual primary
  70.      * delivery state or primary transaction state) when an order is written. If an order is written _with_ a delivery
  71.      * or transaction, the other events (ORDER_DELIVERY_WRITTEN_EVENT and/or ORDER_TRANSACTION_WRITTEN_EVENT) will be
  72.      * triggered as well, which in turn will update the primary states of the order configuration.
  73.      *
  74.      * So in production this lone "ensure order configuration exists" subscriber is relevant for creating orders
  75.      * without order deliveries or order transactions. This way we can ensure that the order configuration extension
  76.      * always exists.
  77.      */
  78.     public function orderWritten(EntityWrittenEvent $event): void
  79.     {
  80.         if ($this->skip) {
  81.             return;
  82.         }
  83.         $orderIds = [];
  84.         foreach ($event->getWriteResults() as $writeResult) {
  85.             $payload $writeResult->getPayload();
  86.             if (!array_key_exists('id'$payload)) {
  87.                 // If an order is deleted, this event is also triggered and there is no ID in the payload. The ON DELETE
  88.                 // CASCADE foreign key will delete the order configuration extension.
  89.                 continue;
  90.             }
  91.             $orderIds[] = $payload['id'];
  92.         }
  93.         $this->ensureOrderConfigurationsExist($orderIds);
  94.     }
  95.     /**
  96.      * This event is dispatched when an order delivery is created or updated (not when it is deleted).
  97.      */
  98.     public function orderDeliveryWritten(EntityWrittenEvent $event): void
  99.     {
  100.         if ($this->skip) {
  101.             return;
  102.         }
  103.         $orderIds $this->getOrderIdsFromOrderAssociationWrittenEvent($event'order_delivery');
  104.         if (count($orderIds) === 0) {
  105.             return;
  106.         }
  107.         RetryableTransaction::retryable($this->connection, function () use ($orderIds$event): void {
  108.             $this->ensureOrderConfigurationsExist($orderIds);
  109.             $this->updatePrimaryOrderDeliveries($orderIds);
  110.             if ($this->eventDispatcher) {
  111.                 $this->eventDispatcher->dispatch(
  112.                     new OrderConfigurationUpdatedEvent($orderIds$event->getContext()),
  113.                     OrderConfigurationUpdatedEvent::EVENT_NAME,
  114.                 );
  115.             }
  116.         });
  117.     }
  118.     /**
  119.      * This event is dispatched when an order transaction is created or updated (not when it is deleted).
  120.      */
  121.     public function orderTransactionWritten(EntityWrittenEvent $event): void
  122.     {
  123.         if ($this->skip) {
  124.             return;
  125.         }
  126.         $orderIds $this->getOrderIdsFromOrderAssociationWrittenEvent($event'order_transaction');
  127.         if (count($orderIds) === 0) {
  128.             return;
  129.         }
  130.         RetryableTransaction::retryable($this->connection, function () use ($orderIds$event): void {
  131.             $this->ensureOrderConfigurationsExist($orderIds);
  132.             $this->updatePrimaryOrderTransactions($orderIds);
  133.             if ($this->eventDispatcher) {
  134.                 $this->eventDispatcher->dispatch(
  135.                     new OrderConfigurationUpdatedEvent($orderIds$event->getContext()),
  136.                     OrderConfigurationUpdatedEvent::EVENT_NAME,
  137.                 );
  138.             }
  139.         });
  140.     }
  141.     private function getOrderIdsFromOrderAssociationWrittenEvent(
  142.         EntityWrittenEvent $event,
  143.         string $orderAssociationTableName
  144.     ): array {
  145.         $ids = [];
  146.         foreach ($event->getWriteResults() as $writeResult) {
  147.             $payload $writeResult->getPayload();
  148.             if (!array_key_exists('id'$payload)) {
  149.                 // Whenever an order delivery or order transaction is written (created or updated), the 'id' must be
  150.                 // present in the payload. We are not 100% sure when or how this scenario occurs when there is no id set
  151.                 // in the payload. But a customer reported it in this SCS Support Ticket 212711.
  152.                 continue;
  153.             }
  154.             $ids[] = $payload['id'];
  155.         }
  156.         if (count($ids) === 0) {
  157.             return [];
  158.         }
  159.         return array_unique(array_values($this->connection->fetchFirstColumn(
  160.             'SELECT LOWER(HEX(`order_id`)) FROM `' $orderAssociationTableName '` WHERE `id` IN (:ids)',
  161.             ['ids' => array_map('hex2bin'$ids)],
  162.             ['ids' => Connection::PARAM_STR_ARRAY],
  163.         )));
  164.     }
  165.     /**
  166.      * @param String[] $orderIds
  167.      */
  168.     public function updateOrderConfigurations(array $orderIdsContext $context): void
  169.     {
  170.         if ($this->skip) {
  171.             return;
  172.         }
  173.         if (count($orderIds) === 0) {
  174.             return;
  175.         }
  176.         RetryableTransaction::retryable($this->connection, function () use ($orderIds$context): void {
  177.             $this->ensureOrderConfigurationsExist($orderIds);
  178.             $this->updatePrimaryOrderDeliveries($orderIds);
  179.             $this->updatePrimaryOrderTransactions($orderIds);
  180.             if ($this->eventDispatcher) {
  181.                 $this->eventDispatcher->dispatch(
  182.                     new OrderConfigurationUpdatedEvent($orderIds$context),
  183.                     OrderConfigurationUpdatedEvent::EVENT_NAME,
  184.                 );
  185.             }
  186.         });
  187.     }
  188.     /**
  189.      * We limit the OrderConfiguration existence to the live version. We needed to make this restriction to avoid
  190.      * deadlocks in a recent feature development. We know that we _actually_ want all order versions to have an order
  191.      * configuration. But the usages are deprecated (and not used anymore) so we ignore this until 6.5 when the whole
  192.      * class is removed once and for all.
  193.      *
  194.      * @param String[] $orderIds
  195.      */
  196.     private function ensureOrderConfigurationsExist(array $orderIds): void
  197.     {
  198.         $this->connection->executeStatement(
  199.             'INSERT INTO `pickware_shopware_extensions_order_configuration`
  200.             (
  201.                 `id`,
  202.                 `version_id`,
  203.                 `order_id`,
  204.                 `order_version_id`,
  205.                 `created_at`
  206.             ) SELECT
  207.                 ' SqlUuid::UUID_V4_GENERATION ',
  208.                 `version_id`,
  209.                 `id`,
  210.                 `version_id`,
  211.                 NOW(3)
  212.             FROM `order`
  213.             WHERE `order`.`id` IN (:orderIds)
  214.             AND `order`.`version_id` = :liveVersion
  215.             ON DUPLICATE KEY UPDATE `pickware_shopware_extensions_order_configuration`.`id` = `pickware_shopware_extensions_order_configuration`.`id`',
  216.             [
  217.                 'orderIds' => array_map('hex2bin'$orderIds),
  218.                 'liveVersion' => hex2bin(Defaults::LIVE_VERSION),
  219.             ],
  220.             ['orderIds' => Connection::PARAM_STR_ARRAY],
  221.         );
  222.     }
  223.     /**
  224.      * The order transactions and order deliveries are referenced in the
  225.      * `pickware_shopware_extensions_order_configuration` table. If such a reference is deleted, the respective
  226.      * reference field is nulled due to the ON DELETE SET NULL foreign key. In the ENTITY_DELETED event we have no way
  227.      * of knowing which order we have to update, because all references are gone at that point in time. Therefore, we
  228.      * check every order that has a null reference on a primary order transaction or order delivery. These should be
  229.      * only a few, ideally only the order of the recently deleted reference, because there should be no orders without
  230.      * order deliveries or order transactions in production.
  231.      *
  232.      * It is also possible that a non-primary order delivery or non-primary order transaction was deleted when this
  233.      * subscriber was triggered and this method will return early without any update.
  234.      */
  235.     public function updateOrderConfigurationAfterDeliveryOrTransactionDeletion(EntityWrittenEvent $event): void
  236.     {
  237.         if ($this->skip) {
  238.             return;
  239.         }
  240.         RetryableTransaction::retryable($this->connection, function () use ($event): void {
  241.             $orderIds $this->connection->fetchFirstColumn(
  242.                 'SELECT LOWER(HEX(`order_id`)) FROM `pickware_shopware_extensions_order_configuration` orderConfiguration
  243.                 WHERE (
  244.                     orderConfiguration.`primary_order_delivery_id` IS NULL
  245.                     OR orderConfiguration.`primary_order_transaction_id` IS NULL
  246.                 )',
  247.             );
  248.             if (!$orderIds) {
  249.                 return;
  250.             }
  251.             $this->updatePrimaryOrderDeliveries($orderIds);
  252.             $this->updatePrimaryOrderTransactions($orderIds);
  253.             if ($this->eventDispatcher) {
  254.                 $this->eventDispatcher->dispatch(
  255.                     new OrderConfigurationUpdatedEvent($orderIds$event->getContext()),
  256.                     OrderConfigurationUpdatedEvent::EVENT_NAME,
  257.                 );
  258.             }
  259.         });
  260.     }
  261.     /**
  262.      * @param String[] $orderIds
  263.      */
  264.     private function updatePrimaryOrderDeliveries(array $orderIds): void
  265.     {
  266.         $this->connection->executeStatement(
  267.             'UPDATE `pickware_shopware_extensions_order_configuration` orderConfiguration
  268.             LEFT JOIN `order`
  269.                 ON `order`.`id` = orderConfiguration.`order_id`
  270.                 AND `order`.`version_id` = orderConfiguration.`order_version_id`
  271.             -- Select a single order delivery with the highest shippingCosts.unitPrice as the primary order
  272.             -- delivery for the order. This selection strategy is adapted from how order deliveries are selected
  273.             -- in the administration. See /administration/src/module/sw-order/view/sw-order-detail-base/index.js
  274.             LEFT JOIN (
  275.                 SELECT
  276.                     `order_id`,
  277.                     `order_version_id`,
  278.                     MAX(
  279.                         CAST(JSON_UNQUOTE(
  280.                             JSON_EXTRACT(`order_delivery`.`shipping_costs`, "$.unitPrice")
  281.                         ) AS DECIMAL)
  282.                     ) AS `unitPrice`
  283.                 FROM `order_delivery`
  284.                 GROUP BY `order_id`, `order_version_id`
  285.             ) `primary_order_delivery_shipping_cost`
  286.                 ON `primary_order_delivery_shipping_cost`.`order_id` = `order`.`id`
  287.                 AND `primary_order_delivery_shipping_cost`.`order_version_id` = `order`.`version_id`
  288.             LEFT JOIN `order_delivery`
  289.                 ON `order_delivery`.`order_id` = `order`.`id`
  290.                 AND `order_delivery`.`order_version_id` = `order`.`version_id`
  291.                 AND CAST(JSON_UNQUOTE(JSON_EXTRACT(`order_delivery`.`shipping_costs`, "$.unitPrice")) AS DECIMAL) = `primary_order_delivery_shipping_cost`.`unitPrice`
  292.             SET orderConfiguration.`primary_order_delivery_id` = `order_delivery`.`id`,
  293.                 orderConfiguration.`primary_order_delivery_version_id` = `order_delivery`.`version_id`
  294.             WHERE orderConfiguration.`order_id` IN (:orderIds)',
  295.             ['orderIds' => array_map('hex2bin'$orderIds)],
  296.             ['orderIds' => Connection::PARAM_STR_ARRAY],
  297.         );
  298.     }
  299.     /**
  300.      * @param String[] $orderIds
  301.      */
  302.     private function updatePrimaryOrderTransactions(array $orderIds): void
  303.     {
  304.         $this->connection->executeStatement(
  305.             'UPDATE `pickware_shopware_extensions_order_configuration` orderConfiguration
  306.             LEFT JOIN `order`
  307.                 ON `order`.`id` = orderConfiguration.`order_id`
  308.                 AND `order`.`version_id` = orderConfiguration.`order_version_id`
  309.             -- Select oldest order transaction that is not "cancelled" or "failed" else return the last order transaction.
  310.             -- https://github.com/shopware/platform/blob/v6.4.8.1/src/Administration/Resources/app/administration/src/module/sw-order/view/sw-order-detail-base/index.js#L91-L98
  311.             -- https://github.com/shopware/platform/blob/v6.4.8.1/src/Administration/Resources/app/administration/src/module/sw-order/view/sw-order-detail-base/index.js#L207
  312.             LEFT JOIN `order_transaction`
  313.                 ON `order_transaction`.`id` = (
  314.                     SELECT innerOrderTransaction.`id`
  315.                     FROM `order_transaction` innerOrderTransaction
  316.                     LEFT JOIN `state_machine_state` AS innerOrderTransactionState
  317.                         ON innerOrderTransactionState.`id` = innerOrderTransaction.`state_id`
  318.                     WHERE innerOrderTransaction.`order_id` = `order`.`id`
  319.                     AND innerOrderTransaction.`version_id` = `order`.`version_id`
  320.                     ORDER BY
  321.                         IF(innerOrderTransactionState.`technical_name` IN (:ignoredStates), 0, 1) DESC,
  322.                         innerOrderTransaction.created_at ASC
  323.                     LIMIT 1
  324.                 ) AND `order_transaction`.`version_id` = `order`.`version_id`
  325.             SET orderConfiguration.`primary_order_transaction_id` = `order_transaction`.`id`,
  326.                 orderConfiguration.`primary_order_transaction_version_id` = `order_transaction`.`version_id`
  327.             WHERE orderConfiguration.`order_id` IN (:orderIds)',
  328.             [
  329.                 'orderIds' => array_map('hex2bin'$orderIds),
  330.                 'ignoredStates' => OrderTransactionCollectionExtension::PRIMARY_TRANSACTION_IGNORED_STATES,
  331.             ],
  332.             [
  333.                 'orderIds' => Connection::PARAM_STR_ARRAY,
  334.                 'ignoredStates' => Connection::PARAM_STR_ARRAY,
  335.             ],
  336.         );
  337.     }
  338. }