<?php
declare(strict_types=1);
namespace Vio\B2BWorkflow;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use RuntimeException;
use Shopware\Core\Checkout\Order\OrderDefinition;
use Shopware\Core\Checkout\Order\OrderStates as ShopwareOrderStates;
use Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStates as ShopwareOrderTransactionStates;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\Plugin;
use Shopware\Core\Framework\Plugin\Context\DeactivateContext;
use Shopware\Core\Framework\Plugin\Context\InstallContext;
use Shopware\Core\Framework\Plugin\Context\UninstallContext;
use Shopware\Core\Framework\Plugin\Context\UpdateContext;
use Shopware\Core\System\CustomField\CustomFieldTypes;
use Shopware\Core\System\Language\LanguageDefinition;
use Shopware\Core\System\Language\LanguageEntity;
use Shopware\Core\System\StateMachine\Aggregation\StateMachineState\StateMachineStateDefinition;
use Shopware\Core\System\StateMachine\Aggregation\StateMachineState\StateMachineStateEntity;
use Shopware\Core\System\StateMachine\Aggregation\StateMachineTransition\StateMachineTransitionDefinition;
use Shopware\Core\System\StateMachine\StateMachineDefinition;
use Shopware\Core\System\StateMachine\StateMachineEntity;
use VioB2BLogin\Entity\Employee\EmployeeDefinition;
use VioB2BLogin\VioB2BLogin;
use Vio\B2BWorkflow\Core\Checkout\Order\OrderStates;
use Vio\B2BWorkflow\Core\Checkout\Order\OrderTransactionStates;
use Vio\B2BWorkflow\Core\DataAbstractionLayer\Search\Filter\ArrayKeyMultiOrFilter;
use Vio\B2BWorkflow\Core\DataAbstractionLayer\Search\Filter\StateMachineFilter;
use Vio\B2BWorkflow\Core\System\StateMachine\Aggregation\StateMachineTransition\StateMachineTransitionActions;
class VioB2BWorkflow extends Plugin
{
public const APPROVAL_EMPLOYEE_FIELD = 'vio_b2b_approval_employee';
private string $enId;
private string $deId;
/**
* @param Context $context
* @param StateMachineEntity $stateMachine
* @param bool $withoutTranslations
* @return array[]
*/
protected function getStates(Context $context, StateMachineEntity $stateMachine, bool $withoutTranslations = false): array
{
$translations = $this->getStateTranslations($context);
$states = [
ShopwareOrderStates::STATE_MACHINE => [
[
'technicalName' => OrderStates::STATE_APPROVAL_REQUESTED,
'stateMachineId' => $stateMachine->getId(),
'translations' => $translations[OrderStates::STATE_APPROVAL_REQUESTED]
],
[
'technicalName' => OrderStates::STATE_APPROVAL_REFUSED,
'stateMachineId' => $stateMachine->getId(),
'translations' => $translations[OrderStates::STATE_APPROVAL_REFUSED]
]
],
ShopwareOrderTransactionStates::STATE_MACHINE => [
[
'technicalName' => OrderTransactionStates::STATE_PRESET,
'stateMachineId' => $stateMachine->getId(),
'translations' => $translations[OrderTransactionStates::STATE_PRESET]
]
]
];
$return = $states[$stateMachine->getTechnicalName()];
if ($withoutTranslations) {
$return = array_map(static function (array $stateArray) {
return [
'technicalName' => $stateArray['technicalName'],
'stateMachineId' => $stateArray['stateMachineId']
];
}, $return);
}
return $return;
}
/**
* @param Context $context
* @return array
*/
protected function getStateTranslations(Context $context): array
{
$defaultLangId = Defaults::LANGUAGE_SYSTEM;
$deDE = $this->getDeDeLanguageId($context);
$enGB = $this->getEnGbLanguageId($context);
$translations = [];
$translations[OrderStates::STATE_APPROVAL_REQUESTED] = [
$deDE => ['name' => 'Freigabe beantragt'],
$enGB => ['name' => 'Approval requested']
];
$translations[OrderStates::STATE_APPROVAL_REFUSED] = [
$deDE => ['name' => 'Freigabe abgelehnt'],
$enGB => ['name' => 'Approval refused']
];
$translations[OrderTransactionStates::STATE_PRESET] = [
$deDE => ['name' => 'Voreingestellt'],
$enGB => ['name' => 'Preset']
];
if ($defaultLangId !== $deDE) {
$translations[OrderStates::STATE_APPROVAL_REQUESTED][$defaultLangId] = ['name' => 'Approval requested'];
$translations[OrderStates::STATE_APPROVAL_REFUSED][$defaultLangId] = ['name' => 'Approval refused'];
$translations[OrderTransactionStates::STATE_PRESET][$defaultLangId] = ['name' => 'Preset'];
}
return $translations;
}
/**
* @param StateMachineEntity $stateMachine
* @param string[] $states
* @return array[]
*/
private function getTransitions(StateMachineEntity $stateMachine, array $states = []): array
{
$transitions = [
ShopwareOrderStates::STATE_MACHINE => [
[
'actionName' => StateMachineTransitionActions::ACTION_REFUSE,
'stateMachineId' => $stateMachine->getId(),
'fromStateId' => array_key_exists(OrderStates::STATE_APPROVAL_REQUESTED, $states) ? $states[OrderStates::STATE_APPROVAL_REQUESTED] : '',
'toStateId' => array_key_exists(OrderStates::STATE_APPROVAL_REFUSED, $states) ? $states[OrderStates::STATE_APPROVAL_REFUSED] : '',
],
[
'actionName' => StateMachineTransitionActions::ACTION_ACCEPT,
'stateMachineId' => $stateMachine->getId(),
'fromStateId' => array_key_exists(OrderStates::STATE_APPROVAL_REQUESTED, $states) ? $states[OrderStates::STATE_APPROVAL_REQUESTED] : '',
'toStateId' => array_key_exists(ShopwareOrderStates::STATE_OPEN, $states) ? $states[ShopwareOrderStates::STATE_OPEN] : '',
],
[
'actionName' => StateMachineTransitionActions::ACTION_RESTORE,
'stateMachineId' => $stateMachine->getId(),
'fromStateId' => array_key_exists(OrderStates::STATE_APPROVAL_REFUSED, $states) ? $states[OrderStates::STATE_APPROVAL_REFUSED] : '',
'toStateId' => array_key_exists(OrderStates::STATE_APPROVAL_REQUESTED, $states) ? $states[OrderStates::STATE_APPROVAL_REQUESTED] : '',
]
],
ShopwareOrderTransactionStates::STATE_MACHINE => [
[
'actionName' => StateMachineTransitionActions::ACTION_ACCEPT,
'stateMachineId' => $stateMachine->getId(),
'fromStateId' => array_key_exists(OrderTransactionStates::STATE_PRESET, $states) ? $states[OrderTransactionStates::STATE_PRESET] : '',
'toStateId' => array_key_exists(ShopwareOrderTransactionStates::STATE_OPEN, $states) ? $states[ShopwareOrderTransactionStates::STATE_OPEN] : '',
]
],
];
$return = $transitions[$stateMachine->getTechnicalName()];
if (empty($states)) {
$return = array_map(static function (array $transitionArray) {
return [
'actionName' => $transitionArray['actionName'],
'stateMachineId' => $transitionArray['stateMachineId']
];
}, $return);
}
return $return;
}
#region setup
public function install(InstallContext $installContext): void
{
parent::install($installContext);
$this->installOrUpdateStates($installContext->getContext());
$this->updateCustomFields($installContext->getContext());
}
public function update(UpdateContext $updateContext): void
{
parent::update($updateContext);
$this->installOrUpdateStates($updateContext->getContext());
$this->updateCustomFields($updateContext->getContext());
}
public function deactivate(DeactivateContext $deactivateContext): void
{
$this->removeCustomFields($deactivateContext->getContext());
parent::deactivate($deactivateContext);
}
public function uninstall(UninstallContext $uninstallContext): void
{
if (!$uninstallContext->keepUserData()) {
$this->uninstallStates($uninstallContext->getContext());
}
parent::uninstall($uninstallContext);
}
#endregion
#region setup-helper
public function getDeDeLanguageId(Context $context): string
{
if (empty($this->deId)) {
/** @var EntityRepositoryInterface $repository */
$repository = $this->container->get(LanguageDefinition::ENTITY_NAME . '.repository');
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter(LanguageDefinition::ENTITY_NAME . '.translationCode.code', 'de-DE'));
/** @var LanguageEntity $language */
$language = $repository->search($criteria, $context)->first();
$this->deId = $language->getId();
}
return $this->deId;
}
public function getEnGbLanguageId(Context $context): string
{
if (empty($this->enId)) {
/** @var EntityRepositoryInterface $repository */
$repository = $this->container->get(LanguageDefinition::ENTITY_NAME . '.repository');
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter(LanguageDefinition::ENTITY_NAME . '.translationCode.code', 'en-GB'));
/** @var LanguageEntity $language */
$language = $repository->search($criteria, $context)->first();
$this->enId = $language->getId();
}
return $this->enId;
}
/**
* @param Context $context
*/
private function installOrUpdateStates(Context $context): void
{
// find order state machine
/** @var EntityRepository $stateMachineRepo */
$stateMachineRepo = $this->container->get(StateMachineDefinition::ENTITY_NAME . '.repository');
$criteria = (new Criteria())
->addFilter(new EqualsFilter('technicalName', ShopwareOrderStates::STATE_MACHINE));
$stateMachinesResult = $stateMachineRepo->search($criteria, $context);
if ($stateMachinesResult->getTotal() === 1) {
$stateMachine = $stateMachinesResult->first();
$this->insertOrUpdateStateForStateMachine($stateMachine, $context);
}
// find order transaction state machine
$criteria = (new Criteria())
->addFilter(new EqualsFilter('technicalName', ShopwareOrderTransactionStates::STATE_MACHINE));
$stateMachinesResult = $stateMachineRepo->search($criteria, $context);
if ($stateMachinesResult->getTotal() === 1) {
$stateMachine = $stateMachinesResult->first();
$this->insertOrUpdateStateForStateMachine($stateMachine, $context);
}
}
/**
* @param Context $context
*/
private function uninstallStates(Context $context): void
{
// find order state machine
/** @var EntityRepository $stateMachineRepo */
$stateMachineRepo = $this->container->get(StateMachineDefinition::ENTITY_NAME . '.repository');
$criteria = (new Criteria())
->addFilter(new EqualsFilter('technicalName', ShopwareOrderStates::STATE_MACHINE));
$stateMachinesResult = $stateMachineRepo->search($criteria, $context);
if ($stateMachinesResult->getTotal() === 1) {
/** @var StateMachineEntity $stateMachine */
$stateMachine = $stateMachinesResult->first();
$this->uninstallStatesByStateMachine($stateMachine, $context);
}
}
/**
* @param array $states
* @param StateMachineEntity $stateMachine
* @param Context $context
* @return void
*/
private function createTransitions(array $states, StateMachineEntity $stateMachine, Context $context): void
{
/** @var EntityRepository $transitionRepository */
$transitionRepository = $this->container->get(StateMachineTransitionDefinition::ENTITY_NAME . '.repository');
$transitions = $this->getTransitions($stateMachine, $states);
$transitions = array_values($this->validateTransitionsBeforeInsert($transitions, $stateMachine, $context));
$transitionRepository->upsert($transitions, $context);
}
/**
* @param StateMachineEntity $stateMachine
* @param Context $context
* @return void
*/
private function deleteTransitions(StateMachineEntity $stateMachine, Context $context): void
{
/** @var EntityRepository $transitionRepository */
$transitionRepository = $this->container->get(StateMachineTransitionDefinition::ENTITY_NAME . '.repository');
$transitions = $this->getTransitions($stateMachine);
$criteria = new Criteria();
$criteria->addFilter(new StateMachineFilter($stateMachine, $transitions, 'actionName'));
$result = $transitionRepository->searchIds($criteria, $context);
$delete = array_values(array_map(static function ($id) {
return ['id' => $id];
}, $result->getIds()));
if (count($delete) > 0) {
$transitionRepository->delete($delete, $context);
}
}
/**
* @param array $upsertStates
* @param StateMachineEntity $stateMachine
* @param Context $context
* @return array
*/
private function validateStatesBeforeInsert(array $upsertStates, StateMachineEntity $stateMachine, Context $context): array
{
$criteria = new Criteria();
$criteria->addFilter(new StateMachineFilter($stateMachine, $upsertStates, 'technicalName'));
/** @var EntityRepository $repository */
$repository = $this->container->get(StateMachineStateDefinition::ENTITY_NAME . '.repository');
$result = $repository->search($criteria, $context);
return array_filter($upsertStates, static function (array $state) use ($result) {
return $result->filterByProperty('technicalName', $state['technicalName'])->count() === 0;
});
}
/**
* @param array $transitions
* @param StateMachineEntity $stateMachine
* @param Context $context
* @return array
*/
private function validateTransitionsBeforeInsert(array $transitions, StateMachineEntity $stateMachine, Context $context): array
{
$criteria = new Criteria();
$criteria->addFilter(new StateMachineFilter($stateMachine, $transitions, 'actionName'));
/** @var EntityRepository $repository */
$repository = $this->container->get(StateMachineTransitionDefinition::ENTITY_NAME . '.repository');
$result = $repository->search($criteria, $context);
return array_filter($transitions, static function (array $state) use ($result) {
return $result->filterByProperty('actionName', $state['actionName'])->count() === 0;
});
}
#endregion
/**
* @param StateMachineEntity $stateMachine
* @param Context $context
*/
private function insertOrUpdateStateForStateMachine(StateMachineEntity $stateMachine, Context $context): void
{
/** @var EntityRepository $repository */
$repository = $this->container->get(StateMachineStateDefinition::ENTITY_NAME . '.repository');
$statesToUpsert = $this->getStates($context, $stateMachine);
$upsertStates = array_values($this->validateStatesBeforeInsert($statesToUpsert, $stateMachine, $context));
$repository->upsert($upsertStates, $context);
$states = [];
$filter = new ArrayKeyMultiOrFilter($statesToUpsert, 'technicalName');
$filter->addQuery(new EqualsFilter('technicalName', ShopwareOrderStates::STATE_OPEN));
$criteria = (new Criteria())
->addFilter(new EqualsFilter('stateMachineId', $stateMachine->getId()))
->addFilter($filter);
$statesResult = $repository->search($criteria, $context);
if ($statesResult->getTotal() > 0) {
/** @var StateMachineStateEntity $state */
foreach ($statesResult->getElements() as $state) {
$states[$state->getTechnicalName()] = $state->getId();
}
}
$this->createTransitions($states, $stateMachine, $context);
}
/**
* @param StateMachineEntity $stateMachine
* @param Context $context
*/
private function uninstallStatesByStateMachine(StateMachineEntity $stateMachine, Context $context): void
{
$this->deleteTransitions($stateMachine, $context);
/** @var EntityRepository $repository */
$repository = $this->container->get(StateMachineStateDefinition::ENTITY_NAME . '.repository');
$deleteStates = $this->getStates($context, $stateMachine, true);
$criteria = new Criteria();
$criteria->addFilter(new StateMachineFilter($stateMachine, $deleteStates, 'technicalName'));
$result = $repository->searchIds($criteria, $context);
$delete = [];
// check every state if it is used in an order
foreach ($result->getIds() as $stateId) {
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('stateId', $stateId));
/** @var EntityRepository $orderRepository */
$orderRepository = $this->container->get(OrderDefinition::ENTITY_NAME . '.repository');
$orderResult = $orderRepository->searchIds($criteria, $context);
if ($orderResult->getTotal() === 0) {
$delete[] = ['id' => $stateId];
}
}
if (count($delete) > 0) {
$repository->delete(
$delete,
$context
);
}
}
/**
* @param Context $context
*/
private function updateCustomFields(Context $context): void
{
$customFieldSetRepository = $this->container->get('custom_field_set.repository');
if (!$customFieldSetRepository) {
throw new RuntimeException("Couldn't resolve service 'custom_field_set.repository'");
}
$fieldSetName = VioB2BLogin::CUSTOM_FIELDSET_NAME;
$fieldSetId = $customFieldSetRepository->searchIds((new Criteria())->addFilter(new EqualsFilter('name', $fieldSetName)), $context)->firstId();
if ($fieldSetId !== null) {
$this->updateCustomField(
$fieldSetId,
static::APPROVAL_EMPLOYEE_FIELD,
[
'name' => static::APPROVAL_EMPLOYEE_FIELD,
'type' => CustomFieldTypes::ENTITY,
'config' => [
'label' => [
'de-DE' => 'Freigebender Mitarbeiter',
'en-GB' => 'Approval employee'
],
'entity' => EmployeeDefinition::ENTITY_NAME,
'componentName' => 'sw-entity-single-select',
'labelProperty' => ['firstName', 'lastName']
],
],
$context
);
}
}
/**
* @param string $fieldSetId
* @param string $fieldName
* @param array $fieldData
* @param Context $context
* @noinspection PhpSameParameterValueInspection
*/
private function updateCustomField(string $fieldSetId, string $fieldName, array $fieldData, Context $context): void
{
$customFieldRepository = $this->container->get('custom_field.repository');
if (!$customFieldRepository) {
throw new RuntimeException("Couldn't resolve service 'custom_field.repository'");
}
if (!array_key_exists('name', $fieldData)) {
$fieldData['name'] = $fieldName;
}
if (!array_key_exists('customFieldSetId', $fieldData)) {
$fieldData['customFieldSetId'] = $fieldSetId;
}
$fieldId = $customFieldRepository->searchIds(
(new Criteria())->addFilter(
new EqualsFilter('name', $fieldName),
new EqualsFilter('customFieldSetId', $fieldSetId)
), $context)
->firstId();
if ($fieldId !== null) {
$fieldData['id'] = $fieldId;
}
$customFieldRepository->upsert([$fieldData], $context);
}
private function removeCustomFields(Context $context): void
{
$customFieldSetRepository = $this->container->get('custom_field_set.repository');
if (!$customFieldSetRepository) {
throw new RuntimeException("Couldn't resolve service 'custom_field_set.repository'");
}
$fieldSetName = VioB2BLogin::CUSTOM_FIELDSET_NAME;
$fieldSetId = $customFieldSetRepository->searchIds((new Criteria())->addFilter(new EqualsFilter('name', $fieldSetName)), $context)->firstId();
if ($fieldSetId !== null) {
$customFieldRepository = $this->container->get('custom_field.repository');
if (!$customFieldRepository) {
throw new RuntimeException("Couldn't resolve service 'custom_field_set.repository'");
}
$fieldId = $customFieldRepository->searchIds(
(new Criteria())->addFilter(
new EqualsFilter('name', static::APPROVAL_EMPLOYEE_FIELD),
new EqualsFilter('customFieldSetId', $fieldSetId)
), $context)
->firstId();
if ($fieldId) {
$customFieldRepository->delete([
[
'id' => $fieldId
]
], $context);
}
}
}
}