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

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 Shopware\Core\Checkout\Order\OrderEvents;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
  16. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  17. class OrderConfigurationUpdater implements EventSubscriberInterface
  18. {
  19.     private Connection $connection;
  20.     public function __construct(Connection $connection)
  21.     {
  22.         $this->connection $connection;
  23.     }
  24.     public static function getSubscribedEvents(): array
  25.     {
  26.         return [
  27.             OrderEvents::ORDER_WRITTEN_EVENT => 'orderWritten',
  28.             OrderEvents::ORDER_DELIVERY_WRITTEN_EVENT => 'orderDeliveryWritten',
  29.             OrderEvents::ORDER_DELIVERY_DELETED_EVENT => 'updateOrderConfigurationAfterDeliveryOrTransactionDeletion',
  30.             OrderEvents::ORDER_TRANSACTION_WRITTEN_EVENT => 'orderTransactionWritten',
  31.             OrderEvents::ORDER_TRANSACTION_DELETED_EVENT => 'updateOrderConfigurationAfterDeliveryOrTransactionDeletion',
  32.         ];
  33.     }
  34.     /**
  35.      * This subscriber method only ensures that the order configuration exists (without updating the actual primary
  36.      * delivery state or primary transaction state) when an order is written. If an order is written _with_ a delivery
  37.      * or transaction, the other events (ORDER_DELIVERY_WRITTEN_EVENT and/or ORDER_TRANSACTION_WRITTEN_EVENT) will be
  38.      * triggered as well, which in turn will update the primary states of the order configuration.
  39.      *
  40.      * So in production this lone "ensure order configuration exists" subscriber is relevant for creating orders
  41.      * without order deliveries or order transactions. This way we can ensure that the order configuration extension
  42.      * always exists.
  43.      */
  44.     public function orderWritten(EntityWrittenEvent $event): void
  45.     {
  46.         $orderIds = [];
  47.         foreach ($event->getWriteResults() as $writeResult) {
  48.             $payload $writeResult->getPayload();
  49.             if (!array_key_exists('id'$payload)) {
  50.                 // If an order is deleted, this event is also triggered and there is no ID in the payload. The ON DELETE
  51.                 // CASCADE foreign key will delete the order configuration extension.
  52.                 continue;
  53.             }
  54.             $orderIds[] = $payload['id'];
  55.         }
  56.         $this->ensureOrderConfigurationsExist($orderIds);
  57.     }
  58.     /**
  59.      * This event is dispatched when an order delivery is created or updated (not when it is deleted).
  60.      */
  61.     public function orderDeliveryWritten(EntityWrittenEvent $event): void
  62.     {
  63.         $orderIds $this->getOrderIdsFromOrderAssociationWrittenEvent($event'order_delivery');
  64.         if (count($orderIds) === 0) {
  65.             return;
  66.         }
  67.         RetryableTransaction::retryable($this->connection, function () use ($orderIds): void {
  68.             $this->ensureOrderConfigurationsExist($orderIds);
  69.             $this->updatePrimaryOrderDeliveries($orderIds);
  70.         });
  71.     }
  72.     /**
  73.      * This event is dispatched when an order transaction is created or updated (not when it is deleted).
  74.      */
  75.     public function orderTransactionWritten(EntityWrittenEvent $event): void
  76.     {
  77.         $orderIds $this->getOrderIdsFromOrderAssociationWrittenEvent($event'order_transaction');
  78.         if (count($orderIds) === 0) {
  79.             return;
  80.         }
  81.         RetryableTransaction::retryable($this->connection, function () use ($orderIds): void {
  82.             $this->ensureOrderConfigurationsExist($orderIds);
  83.             $this->updatePrimaryOrderTransactions($orderIds);
  84.         });
  85.     }
  86.     private function getOrderIdsFromOrderAssociationWrittenEvent(
  87.         EntityWrittenEvent $event,
  88.         string $orderAssociationTableName
  89.     ): array {
  90.         $ids = [];
  91.         foreach ($event->getWriteResults() as $writeResult) {
  92.             $payload $writeResult->getPayload();
  93.             if (!array_key_exists('id'$payload)) {
  94.                 // Whenever an order delivery or order transaction is written (created or updated), the 'id' must be
  95.                 // present in the payload. We are not 100% sure when or how this scenario occurs when there is no id set
  96.                 // in the payload. But a customer reported it in this SCS Support Ticket 212711.
  97.                 continue;
  98.             }
  99.             $ids[] = $payload['id'];
  100.         }
  101.         if (count($ids) === 0) {
  102.             return [];
  103.         }
  104.         return array_unique(array_values($this->connection->fetchFirstColumn(
  105.             'SELECT LOWER(HEX(`order_id`)) FROM `' $orderAssociationTableName '` WHERE `id` IN (:ids)',
  106.             ['ids' => array_map('hex2bin'$ids)],
  107.             ['ids' => Connection::PARAM_STR_ARRAY],
  108.         )));
  109.     }
  110.     /**
  111.      * @param String[] $orderIds
  112.      */
  113.     public function updateOrderConfigurations(array $orderIds): void
  114.     {
  115.         if (count($orderIds) === 0) {
  116.             return;
  117.         }
  118.         RetryableTransaction::retryable($this->connection, function () use ($orderIds): void {
  119.             $this->ensureOrderConfigurationsExist($orderIds);
  120.             $this->updatePrimaryOrderDeliveries($orderIds);
  121.             $this->updatePrimaryOrderTransactions($orderIds);
  122.         });
  123.     }
  124.     /**
  125.      * @param String[] $orderIds
  126.      */
  127.     private function ensureOrderConfigurationsExist(array $orderIds): void
  128.     {
  129.         $this->connection->executeStatement(
  130.             'INSERT INTO `pickware_shopware_extensions_order_configuration`
  131.             (
  132.                 `id`,
  133.                 `version_id`,
  134.                 `order_id`,
  135.                 `order_version_id`,
  136.                 `created_at`
  137.             ) SELECT
  138.                 ' SqlUuid::UUID_V4_GENERATION ',
  139.                 `version_id`,
  140.                 `id`,
  141.                 `version_id`,
  142.                 NOW(3)
  143.             FROM `order`
  144.             WHERE `order`.`id` IN (:orderIds)
  145.             ON DUPLICATE KEY UPDATE `pickware_shopware_extensions_order_configuration`.`id` = `pickware_shopware_extensions_order_configuration`.`id`',
  146.             ['orderIds' => array_map('hex2bin'$orderIds)],
  147.             ['orderIds' => Connection::PARAM_STR_ARRAY],
  148.         );
  149.     }
  150.     /**
  151.      * The order transactions and order deliveries are referenced in the
  152.      * `pickware_shopware_extensions_order_configuration` table. If such a reference is deleted, the respective
  153.      * reference field is nulled due to the ON DELETE SET NULL foreign key. In the ENTITY_DELETED event we have no way
  154.      * of knowing which order we have to update, because all references are gone at that point in time. Therefore, we
  155.      * check every order that has a null reference on a primary order transaction or order delivery. These should be
  156.      * only a few, ideally only the order of the recently deleted reference, because there should be no orders without
  157.      * order deliveries or order transactions in production.
  158.      *
  159.      * It is also possible that a non-primary order delivery or non-primary order transaction was deleted when this
  160.      * subscriber was triggered and this method will return early without any update.
  161.      */
  162.     public function updateOrderConfigurationAfterDeliveryOrTransactionDeletion(): void
  163.     {
  164.         RetryableTransaction::retryable($this->connection, function (): void {
  165.             $orderIds $this->connection->fetchFirstColumn(
  166.                 'SELECT LOWER(HEX(`order_id`)) FROM `pickware_shopware_extensions_order_configuration` orderConfiguration
  167.                 WHERE (
  168.                     orderConfiguration.`primary_order_delivery_id` IS NULL
  169.                     OR orderConfiguration.`primary_order_transaction_id` IS NULL
  170.                 )',
  171.             );
  172.             if (!$orderIds) {
  173.                 return;
  174.             }
  175.             $this->updatePrimaryOrderDeliveries($orderIds);
  176.             $this->updatePrimaryOrderTransactions($orderIds);
  177.         });
  178.     }
  179.     /**
  180.      * @param String[] $orderIds
  181.      */
  182.     private function updatePrimaryOrderDeliveries(array $orderIds): void
  183.     {
  184.         $this->connection->executeStatement(
  185.             'UPDATE `pickware_shopware_extensions_order_configuration` orderConfiguration
  186.             LEFT JOIN `order`
  187.                 ON `order`.`id` = orderConfiguration.`order_id`
  188.                 AND `order`.`version_id` = orderConfiguration.`order_version_id`
  189.             -- Select a single order delivery with the highest shippingCosts.unitPrice as the primary order
  190.             -- delivery for the order. This selection strategy is adapted from how order deliveries are selected
  191.             -- in the administration. See /administration/src/module/sw-order/view/sw-order-detail-base/index.js
  192.             LEFT JOIN (
  193.                 SELECT
  194.                     `order_id`,
  195.                     `order_version_id`,
  196.                     MAX(
  197.                         CAST(JSON_UNQUOTE(
  198.                             JSON_EXTRACT(`order_delivery`.`shipping_costs`, "$.unitPrice")
  199.                         ) AS DECIMAL)
  200.                     ) AS `unitPrice`
  201.                 FROM `order_delivery`
  202.                 GROUP BY `order_id`, `order_version_id`
  203.             ) `primary_order_delivery_shipping_cost`
  204.                 ON `primary_order_delivery_shipping_cost`.`order_id` = `order`.`id`
  205.                 AND `primary_order_delivery_shipping_cost`.`order_version_id` = `order`.`version_id`
  206.             LEFT JOIN `order_delivery`
  207.                 ON `order_delivery`.`order_id` = `order`.`id`
  208.                 AND `order_delivery`.`order_version_id` = `order`.`version_id`
  209.                 AND CAST(JSON_UNQUOTE(JSON_EXTRACT(`order_delivery`.`shipping_costs`, "$.unitPrice")) AS DECIMAL) = `primary_order_delivery_shipping_cost`.`unitPrice`
  210.             SET orderConfiguration.`primary_order_delivery_id` = `order_delivery`.`id`,
  211.                 orderConfiguration.`primary_order_delivery_version_id` = `order_delivery`.`version_id`
  212.             WHERE orderConfiguration.`order_id` IN (:orderIds)',
  213.             ['orderIds' => array_map('hex2bin'$orderIds)],
  214.             ['orderIds' => Connection::PARAM_STR_ARRAY],
  215.         );
  216.     }
  217.     /**
  218.      * @param String[] $orderIds
  219.      */
  220.     private function updatePrimaryOrderTransactions(array $orderIds): void
  221.     {
  222.         $this->connection->executeStatement(
  223.             'UPDATE `pickware_shopware_extensions_order_configuration` orderConfiguration
  224.             LEFT JOIN `order`
  225.                 ON `order`.`id` = orderConfiguration.`order_id`
  226.                 AND `order`.`version_id` = orderConfiguration.`order_version_id`
  227.             -- Select oldest order transaction that is not "cancelled" or "failed" else return the last order transaction.
  228.             -- 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
  229.             -- 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
  230.             LEFT JOIN `order_transaction`
  231.                 ON `order_transaction`.`id` = (
  232.                     SELECT innerOrderTransaction.`id`
  233.                     FROM `order_transaction` innerOrderTransaction
  234.                     LEFT JOIN `state_machine_state` AS innerOrderTransactionState
  235.                         ON innerOrderTransactionState.`id` = innerOrderTransaction.`state_id`
  236.                     WHERE innerOrderTransaction.`order_id` = `order`.`id`
  237.                     AND innerOrderTransaction.`version_id` = `order`.`version_id`
  238.                     ORDER BY
  239.                         IF((
  240.                             innerOrderTransactionState.`technical_name` = "cancelled" OR
  241.                             innerOrderTransactionState.`technical_name` = "failed"
  242.                         ), 0, 1) DESC,
  243.                         innerOrderTransaction.created_at ASC
  244.                     LIMIT 1
  245.                 ) AND `order_transaction`.`version_id` = `order`.`version_id`
  246.             SET orderConfiguration.`primary_order_transaction_id` = `order_transaction`.`id`,
  247.                 orderConfiguration.`primary_order_transaction_version_id` = `order_transaction`.`version_id`
  248.             WHERE orderConfiguration.`order_id` IN (:orderIds)',
  249.             ['orderIds' => array_map('hex2bin'$orderIds)],
  250.             ['orderIds' => Connection::PARAM_STR_ARRAY],
  251.         );
  252.     }
  253. }