<?php declare(strict_types=1);
namespace Shopware\Core\Content\Flow\Dispatching\Action;
use Doctrine\DBAL\Connection;
use Psr\Log\LoggerInterface;
use Shopware\Core\Checkout\Order\SalesChannel\OrderService;
use Shopware\Core\Content\Flow\Dispatching\DelayableAction;
use Shopware\Core\Content\Flow\Dispatching\StorableFlow;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityDefinitionQueryHelper;
use Shopware\Core\Framework\Event\FlowEvent;
use Shopware\Core\Framework\Event\OrderAware;
use Shopware\Core\Framework\Feature;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\ShopwareHttpException;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\StateMachine\Exception\IllegalTransitionException;
use Shopware\Core\System\StateMachine\Exception\StateMachineNotFoundException;
use Symfony\Component\HttpFoundation\ParameterBag;
/**
* @deprecated tag:v6.5.0 - reason:remove-subscriber - FlowActions won't be executed over the event system anymore,
* therefore the actions won't implement the EventSubscriberInterface anymore.
*/
#[Package('business-ops')]
class SetOrderStateAction extends FlowAction implements DelayableAction
{
public const FORCE_TRANSITION = 'force_transition';
private const ORDER = 'order';
private const ORDER_DELIVERY = 'order_delivery';
private const ORDER_TRANSACTION = 'order_transaction';
private Connection $connection;
private LoggerInterface $logger;
private OrderService $orderService;
/**
* @internal
*/
public function __construct(
Connection $connection,
LoggerInterface $logger,
OrderService $orderService
) {
$this->connection = $connection;
$this->logger = $logger;
$this->orderService = $orderService;
}
public static function getName(): string
{
return 'action.set.order.state';
}
/**
* @deprecated tag:v6.5.0 - reason:remove-subscriber - Will be removed
*
* @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
*/
public static function getSubscribedEvents()
{
if (Feature::isActive('v6.5.0.0')) {
return [];
}
return [
self::getName() => 'handle',
];
}
/**
* @return array<int, string>
*/
public function requirements(): array
{
return [OrderAware::class];
}
/**
* @deprecated tag:v6.5.0 Will be removed, implement handleFlow instead
*/
public function handle(FlowEvent $event): void
{
Feature::triggerDeprecationOrThrow(
'v6.5.0.0',
Feature::deprecatedMethodMessage(__CLASS__, __METHOD__, 'v6.5.0.0')
);
$baseEvent = $event->getEvent();
if (!$baseEvent instanceof OrderAware) {
return;
}
$this->update($baseEvent->getContext(), $event->getConfig(), $baseEvent->getOrderId());
}
public function handleFlow(StorableFlow $flow): void
{
if (!$flow->hasStore(OrderAware::ORDER_ID)) {
return;
}
$this->update($flow->getContext(), $flow->getConfig(), $flow->getStore(OrderAware::ORDER_ID));
}
/**
* @param array<string, mixed> $config
*/
private function update(Context $context, array $config, string $orderId): void
{
if (empty($config)) {
return;
}
if ($config[self::FORCE_TRANSITION] ?? false) {
$context->addState(self::FORCE_TRANSITION);
}
$this->connection->beginTransaction();
try {
$transitions = array_filter([
self::ORDER => $config[self::ORDER] ?? null,
self::ORDER_DELIVERY => $config[self::ORDER_DELIVERY] ?? null,
self::ORDER_TRANSACTION => $config[self::ORDER_TRANSACTION] ?? null,
]);
foreach ($transitions as $machine => $toPlace) {
$this->transitState((string) $machine, $orderId, (string) $toPlace, $context);
}
$this->connection->commit();
} catch (ShopwareHttpException $e) {
$this->connection->rollBack();
$this->logger->error($e->getMessage());
} finally {
$context->removeState(self::FORCE_TRANSITION);
}
}
/**
* @throws IllegalTransitionException
* @throws StateMachineNotFoundException
*/
private function transitState(string $machine, string $orderId, string $toPlace, Context $context): void
{
if (!$toPlace) {
return;
}
$data = new ParameterBag();
$machineId = $machine === self::ORDER ? $orderId : $this->getMachineId($machine, $orderId);
if (!$machineId) {
throw new StateMachineNotFoundException($machine);
}
$actionName = $this->getAvailableActionName($machine, $machineId, $toPlace);
if (!$actionName) {
$actionName = $toPlace;
}
switch ($machine) {
case self::ORDER:
$this->orderService->orderStateTransition($orderId, $actionName, $data, $context);
return;
case self::ORDER_DELIVERY:
$this->orderService->orderDeliveryStateTransition($machineId, $actionName, $data, $context);
return;
case self::ORDER_TRANSACTION:
$this->orderService->orderTransactionStateTransition($machineId, $actionName, $data, $context);
return;
default:
throw new StateMachineNotFoundException($machine);
}
}
private function getMachineId(string $machine, string $orderId): ?string
{
return $this->connection->fetchOne(
'SELECT LOWER(HEX(id)) FROM ' . $machine . ' WHERE order_id = :id AND version_id = :version ORDER BY created_at DESC',
[
'id' => Uuid::fromHexToBytes($orderId),
'version' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION),
]
) ?: null;
}
private function getAvailableActionName(string $machine, string $machineId, string $toPlace): ?string
{
$actionName = $this->connection->fetchOne(
'SELECT action_name FROM state_machine_transition WHERE from_state_id = :fromStateId AND to_state_id = :toPlaceId',
[
'fromStateId' => $this->getFromPlaceId($machine, $machineId),
'toPlaceId' => $this->getToPlaceId($toPlace, $machine),
]
);
return $actionName ?: null;
}
private function getToPlaceId(string $toPlace, string $machine): ?string
{
$id = $this->connection->fetchOne(
'SELECT id FROM state_machine_state WHERE technical_name = :toPlace AND state_machine_id = :stateMachineId',
[
'toPlace' => $toPlace,
'stateMachineId' => $this->getStateMachineId($machine),
]
);
return $id ?: null;
}
private function getFromPlaceId(string $machine, string $machineId): ?string
{
$escaped = EntityDefinitionQueryHelper::escape($machine);
$id = $this->connection->fetchOne(
'SELECT state_id FROM ' . $escaped . 'WHERE id = :id',
[
'id' => Uuid::fromHexToBytes($machineId),
]
);
return $id ?: null;
}
private function getStateMachineId(string $machine): ?string
{
$id = $this->connection->fetchOne(
'SELECT id FROM state_machine WHERE technical_name = :technicalName',
[
'technicalName' => $machine . '.state',
]
);
return $id ?: null;
}
}