vendor/store.shopware.com/viob2bworkflow/src/VioB2BWorkflow.php line 38

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Vio\B2BWorkflow;
  4. use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
  5. use RuntimeException;
  6. use Shopware\Core\Checkout\Order\OrderDefinition;
  7. use Shopware\Core\Checkout\Order\OrderStates as ShopwareOrderStates;
  8. use Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStates as ShopwareOrderTransactionStates;
  9. use Shopware\Core\Defaults;
  10. use Shopware\Core\Framework\Context;
  11. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  12. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  15. use Shopware\Core\Framework\Plugin;
  16. use Shopware\Core\Framework\Plugin\Context\DeactivateContext;
  17. use Shopware\Core\Framework\Plugin\Context\InstallContext;
  18. use Shopware\Core\Framework\Plugin\Context\UninstallContext;
  19. use Shopware\Core\Framework\Plugin\Context\UpdateContext;
  20. use Shopware\Core\System\CustomField\CustomFieldTypes;
  21. use Shopware\Core\System\Language\LanguageDefinition;
  22. use Shopware\Core\System\Language\LanguageEntity;
  23. use Shopware\Core\System\StateMachine\Aggregation\StateMachineState\StateMachineStateDefinition;
  24. use Shopware\Core\System\StateMachine\Aggregation\StateMachineState\StateMachineStateEntity;
  25. use Shopware\Core\System\StateMachine\Aggregation\StateMachineTransition\StateMachineTransitionDefinition;
  26. use Shopware\Core\System\StateMachine\StateMachineDefinition;
  27. use Shopware\Core\System\StateMachine\StateMachineEntity;
  28. use VioB2BLogin\Entity\Employee\EmployeeDefinition;
  29. use VioB2BLogin\VioB2BLogin;
  30. use Vio\B2BWorkflow\Core\Checkout\Order\OrderStates;
  31. use Vio\B2BWorkflow\Core\Checkout\Order\OrderTransactionStates;
  32. use Vio\B2BWorkflow\Core\DataAbstractionLayer\Search\Filter\ArrayKeyMultiOrFilter;
  33. use Vio\B2BWorkflow\Core\DataAbstractionLayer\Search\Filter\StateMachineFilter;
  34. use Vio\B2BWorkflow\Core\System\StateMachine\Aggregation\StateMachineTransition\StateMachineTransitionActions;
  35. class VioB2BWorkflow extends Plugin
  36. {
  37.     public const APPROVAL_EMPLOYEE_FIELD 'vio_b2b_approval_employee';
  38.     private string $enId;
  39.     private string $deId;
  40.     /**
  41.      * @param Context $context
  42.      * @param StateMachineEntity $stateMachine
  43.      * @param bool $withoutTranslations
  44.      * @return array[]
  45.      */
  46.     protected function getStates(Context $contextStateMachineEntity $stateMachinebool $withoutTranslations false): array
  47.     {
  48.         $translations $this->getStateTranslations($context);
  49.         $states = [
  50.             ShopwareOrderStates::STATE_MACHINE => [
  51.                 [
  52.                     'technicalName' => OrderStates::STATE_APPROVAL_REQUESTED,
  53.                     'stateMachineId' => $stateMachine->getId(),
  54.                     'translations' => $translations[OrderStates::STATE_APPROVAL_REQUESTED]
  55.                 ],
  56.                 [
  57.                     'technicalName' => OrderStates::STATE_APPROVAL_REFUSED,
  58.                     'stateMachineId' => $stateMachine->getId(),
  59.                     'translations' => $translations[OrderStates::STATE_APPROVAL_REFUSED]
  60.                 ]
  61.             ],
  62.             ShopwareOrderTransactionStates::STATE_MACHINE => [
  63.                 [
  64.                     'technicalName' => OrderTransactionStates::STATE_PRESET,
  65.                     'stateMachineId' => $stateMachine->getId(),
  66.                     'translations' => $translations[OrderTransactionStates::STATE_PRESET]
  67.                 ]
  68.             ]
  69.         ];
  70.         $return $states[$stateMachine->getTechnicalName()];
  71.         if ($withoutTranslations) {
  72.             $return array_map(static function (array $stateArray) {
  73.                 return [
  74.                     'technicalName' => $stateArray['technicalName'],
  75.                     'stateMachineId' => $stateArray['stateMachineId']
  76.                 ];
  77.             }, $return);
  78.         }
  79.         return $return;
  80.     }
  81.     /**
  82.      * @param Context $context
  83.      * @return array
  84.      */
  85.     protected function getStateTranslations(Context $context): array
  86.     {
  87.         $defaultLangId Defaults::LANGUAGE_SYSTEM;
  88.         $deDE $this->getDeDeLanguageId($context);
  89.         $enGB $this->getEnGbLanguageId($context);
  90.         $translations = [];
  91.         $translations[OrderStates::STATE_APPROVAL_REQUESTED] = [
  92.             $deDE => ['name' => 'Freigabe beantragt'],
  93.             $enGB => ['name' => 'Approval requested']
  94.         ];
  95.         $translations[OrderStates::STATE_APPROVAL_REFUSED] = [
  96.             $deDE => ['name' => 'Freigabe abgelehnt'],
  97.             $enGB => ['name' => 'Approval refused']
  98.         ];
  99.         $translations[OrderTransactionStates::STATE_PRESET] = [
  100.             $deDE => ['name' => 'Voreingestellt'],
  101.             $enGB => ['name' => 'Preset']
  102.         ];
  103.         if ($defaultLangId !== $deDE) {
  104.             $translations[OrderStates::STATE_APPROVAL_REQUESTED][$defaultLangId] = ['name' => 'Approval requested'];
  105.             $translations[OrderStates::STATE_APPROVAL_REFUSED][$defaultLangId] = ['name' => 'Approval refused'];
  106.             $translations[OrderTransactionStates::STATE_PRESET][$defaultLangId] = ['name' => 'Preset'];
  107.         }
  108.         return $translations;
  109.     }
  110.     /**
  111.      * @param StateMachineEntity $stateMachine
  112.      * @param string[] $states
  113.      * @return array[]
  114.      */
  115.     private function getTransitions(StateMachineEntity $stateMachine, array $states = []): array
  116.     {
  117.         $transitions = [
  118.             ShopwareOrderStates::STATE_MACHINE => [
  119.                 [
  120.                     'actionName' => StateMachineTransitionActions::ACTION_REFUSE,
  121.                     'stateMachineId' => $stateMachine->getId(),
  122.                     'fromStateId' => array_key_exists(OrderStates::STATE_APPROVAL_REQUESTED$states) ? $states[OrderStates::STATE_APPROVAL_REQUESTED] : '',
  123.                     'toStateId' => array_key_exists(OrderStates::STATE_APPROVAL_REFUSED$states) ? $states[OrderStates::STATE_APPROVAL_REFUSED] : '',
  124.                 ],
  125.                 [
  126.                     'actionName' => StateMachineTransitionActions::ACTION_ACCEPT,
  127.                     'stateMachineId' => $stateMachine->getId(),
  128.                     'fromStateId' => array_key_exists(OrderStates::STATE_APPROVAL_REQUESTED$states) ? $states[OrderStates::STATE_APPROVAL_REQUESTED] : '',
  129.                     'toStateId' => array_key_exists(ShopwareOrderStates::STATE_OPEN$states) ? $states[ShopwareOrderStates::STATE_OPEN] : '',
  130.                 ],
  131.                 [
  132.                     'actionName' => StateMachineTransitionActions::ACTION_RESTORE,
  133.                     'stateMachineId' => $stateMachine->getId(),
  134.                     'fromStateId' => array_key_exists(OrderStates::STATE_APPROVAL_REFUSED$states) ? $states[OrderStates::STATE_APPROVAL_REFUSED] : '',
  135.                     'toStateId' => array_key_exists(OrderStates::STATE_APPROVAL_REQUESTED$states) ? $states[OrderStates::STATE_APPROVAL_REQUESTED] : '',
  136.                 ]
  137.             ],
  138.             ShopwareOrderTransactionStates::STATE_MACHINE => [
  139.                 [
  140.                     'actionName' => StateMachineTransitionActions::ACTION_ACCEPT,
  141.                     'stateMachineId' => $stateMachine->getId(),
  142.                     'fromStateId' => array_key_exists(OrderTransactionStates::STATE_PRESET$states) ? $states[OrderTransactionStates::STATE_PRESET] : '',
  143.                     'toStateId' => array_key_exists(ShopwareOrderTransactionStates::STATE_OPEN$states) ? $states[ShopwareOrderTransactionStates::STATE_OPEN] : '',
  144.                 ]
  145.             ],
  146.         ];
  147.         $return $transitions[$stateMachine->getTechnicalName()];
  148.         if (empty($states)) {
  149.             $return array_map(static function (array $transitionArray) {
  150.                 return [
  151.                     'actionName' => $transitionArray['actionName'],
  152.                     'stateMachineId' => $transitionArray['stateMachineId']
  153.                 ];
  154.             }, $return);
  155.         }
  156.         return $return;
  157.     }
  158.     #region setup
  159.     public function install(InstallContext $installContext): void
  160.     {
  161.         parent::install($installContext);
  162.         $this->installOrUpdateStates($installContext->getContext());
  163.         $this->updateCustomFields($installContext->getContext());
  164.     }
  165.     public function update(UpdateContext $updateContext): void
  166.     {
  167.         parent::update($updateContext);
  168.         $this->installOrUpdateStates($updateContext->getContext());
  169.         $this->updateCustomFields($updateContext->getContext());
  170.     }
  171.     public function deactivate(DeactivateContext $deactivateContext): void
  172.     {
  173.         $this->removeCustomFields($deactivateContext->getContext());
  174.         parent::deactivate($deactivateContext);
  175.     }
  176.     public function uninstall(UninstallContext $uninstallContext): void
  177.     {
  178.         if (!$uninstallContext->keepUserData()) {
  179.             $this->uninstallStates($uninstallContext->getContext());
  180.         }
  181.         parent::uninstall($uninstallContext);
  182.     }
  183.     #endregion
  184.     #region setup-helper
  185.     public function getDeDeLanguageId(Context $context): string
  186.     {
  187.         if (empty($this->deId)) {
  188.             /** @var EntityRepositoryInterface $repository */
  189.             $repository $this->container->get(LanguageDefinition::ENTITY_NAME '.repository');
  190.             $criteria = new Criteria();
  191.             $criteria->addFilter(new EqualsFilter(LanguageDefinition::ENTITY_NAME '.translationCode.code''de-DE'));
  192.             /** @var LanguageEntity $language */
  193.             $language $repository->search($criteria$context)->first();
  194.             $this->deId $language->getId();
  195.         }
  196.         return $this->deId;
  197.     }
  198.     public function getEnGbLanguageId(Context $context): string
  199.     {
  200.         if (empty($this->enId)) {
  201.             /** @var EntityRepositoryInterface $repository */
  202.             $repository $this->container->get(LanguageDefinition::ENTITY_NAME '.repository');
  203.             $criteria = new Criteria();
  204.             $criteria->addFilter(new EqualsFilter(LanguageDefinition::ENTITY_NAME '.translationCode.code''en-GB'));
  205.             /** @var LanguageEntity $language */
  206.             $language $repository->search($criteria$context)->first();
  207.             $this->enId $language->getId();
  208.         }
  209.         return $this->enId;
  210.     }
  211.     /**
  212.      * @param Context $context
  213.      */
  214.     private function installOrUpdateStates(Context $context): void
  215.     {
  216.         // find order state machine
  217.         /** @var EntityRepository $stateMachineRepo */
  218.         $stateMachineRepo $this->container->get(StateMachineDefinition::ENTITY_NAME '.repository');
  219.         $criteria = (new Criteria())
  220.             ->addFilter(new EqualsFilter('technicalName'ShopwareOrderStates::STATE_MACHINE));
  221.         $stateMachinesResult $stateMachineRepo->search($criteria$context);
  222.         if ($stateMachinesResult->getTotal() === 1) {
  223.             $stateMachine $stateMachinesResult->first();
  224.             $this->insertOrUpdateStateForStateMachine($stateMachine$context);
  225.         }
  226.         // find order transaction state machine
  227.         $criteria = (new Criteria())
  228.             ->addFilter(new EqualsFilter('technicalName'ShopwareOrderTransactionStates::STATE_MACHINE));
  229.         $stateMachinesResult $stateMachineRepo->search($criteria$context);
  230.         if ($stateMachinesResult->getTotal() === 1) {
  231.             $stateMachine $stateMachinesResult->first();
  232.             $this->insertOrUpdateStateForStateMachine($stateMachine$context);
  233.         }
  234.     }
  235.     /**
  236.      * @param Context $context
  237.      */
  238.     private function uninstallStates(Context $context): void
  239.     {
  240. // find order state machine
  241.         /** @var EntityRepository $stateMachineRepo */
  242.         $stateMachineRepo $this->container->get(StateMachineDefinition::ENTITY_NAME '.repository');
  243.         $criteria = (new Criteria())
  244.             ->addFilter(new EqualsFilter('technicalName'ShopwareOrderStates::STATE_MACHINE));
  245.         $stateMachinesResult $stateMachineRepo->search($criteria$context);
  246.         if ($stateMachinesResult->getTotal() === 1) {
  247.             /** @var StateMachineEntity $stateMachine */
  248.             $stateMachine $stateMachinesResult->first();
  249.             $this->uninstallStatesByStateMachine($stateMachine$context);
  250.         }
  251.     }
  252.     /**
  253.      * @param array $states
  254.      * @param StateMachineEntity $stateMachine
  255.      * @param Context $context
  256.      * @return void
  257.      */
  258.     private function createTransitions(array $statesStateMachineEntity $stateMachineContext $context): void
  259.     {
  260.         /** @var EntityRepository $transitionRepository */
  261.         $transitionRepository $this->container->get(StateMachineTransitionDefinition::ENTITY_NAME '.repository');
  262.         $transitions $this->getTransitions($stateMachine$states);
  263.         $transitions array_values($this->validateTransitionsBeforeInsert($transitions$stateMachine$context));
  264.         $transitionRepository->upsert($transitions$context);
  265.     }
  266.     /**
  267.      * @param StateMachineEntity $stateMachine
  268.      * @param Context $context
  269.      * @return void
  270.      */
  271.     private function deleteTransitions(StateMachineEntity $stateMachineContext $context): void
  272.     {
  273.         /** @var EntityRepository $transitionRepository */
  274.         $transitionRepository $this->container->get(StateMachineTransitionDefinition::ENTITY_NAME '.repository');
  275.         $transitions $this->getTransitions($stateMachine);
  276.         $criteria = new Criteria();
  277.         $criteria->addFilter(new StateMachineFilter($stateMachine$transitions'actionName'));
  278.         $result $transitionRepository->searchIds($criteria$context);
  279.         $delete array_values(array_map(static function ($id) {
  280.             return ['id' => $id];
  281.         }, $result->getIds()));
  282.         if (count($delete) > 0) {
  283.             $transitionRepository->delete($delete$context);
  284.         }
  285.     }
  286.     /**
  287.      * @param array $upsertStates
  288.      * @param StateMachineEntity $stateMachine
  289.      * @param Context $context
  290.      * @return array
  291.      */
  292.     private function validateStatesBeforeInsert(array $upsertStatesStateMachineEntity $stateMachineContext $context): array
  293.     {
  294.         $criteria = new Criteria();
  295.         $criteria->addFilter(new StateMachineFilter($stateMachine$upsertStates'technicalName'));
  296.         /** @var EntityRepository $repository */
  297.         $repository $this->container->get(StateMachineStateDefinition::ENTITY_NAME '.repository');
  298.         $result $repository->search($criteria$context);
  299.         return array_filter($upsertStates, static function (array $state) use ($result) {
  300.             return $result->filterByProperty('technicalName'$state['technicalName'])->count() === 0;
  301.         });
  302.     }
  303.     /**
  304.      * @param array $transitions
  305.      * @param StateMachineEntity $stateMachine
  306.      * @param Context $context
  307.      * @return array
  308.      */
  309.     private function validateTransitionsBeforeInsert(array $transitionsStateMachineEntity $stateMachineContext $context): array
  310.     {
  311.         $criteria = new Criteria();
  312.         $criteria->addFilter(new StateMachineFilter($stateMachine$transitions'actionName'));
  313.         /** @var EntityRepository $repository */
  314.         $repository $this->container->get(StateMachineTransitionDefinition::ENTITY_NAME '.repository');
  315.         $result $repository->search($criteria$context);
  316.         return array_filter($transitions, static function (array $state) use ($result) {
  317.             return $result->filterByProperty('actionName'$state['actionName'])->count() === 0;
  318.         });
  319.     }
  320.     #endregion
  321.     /**
  322.      * @param StateMachineEntity $stateMachine
  323.      * @param Context $context
  324.      */
  325.     private function insertOrUpdateStateForStateMachine(StateMachineEntity $stateMachineContext $context): void
  326.     {
  327.         /** @var EntityRepository $repository */
  328.         $repository $this->container->get(StateMachineStateDefinition::ENTITY_NAME '.repository');
  329.         $statesToUpsert $this->getStates($context$stateMachine);
  330.         $upsertStates array_values($this->validateStatesBeforeInsert($statesToUpsert$stateMachine$context));
  331.         $repository->upsert($upsertStates$context);
  332.         $states = [];
  333.         $filter = new ArrayKeyMultiOrFilter($statesToUpsert'technicalName');
  334.         $filter->addQuery(new EqualsFilter('technicalName'ShopwareOrderStates::STATE_OPEN));
  335.         $criteria = (new Criteria())
  336.             ->addFilter(new EqualsFilter('stateMachineId'$stateMachine->getId()))
  337.             ->addFilter($filter);
  338.         $statesResult $repository->search($criteria$context);
  339.         if ($statesResult->getTotal() > 0) {
  340.             /** @var StateMachineStateEntity $state */
  341.             foreach ($statesResult->getElements() as $state) {
  342.                 $states[$state->getTechnicalName()] = $state->getId();
  343.             }
  344.         }
  345.         $this->createTransitions($states$stateMachine$context);
  346.     }
  347.     /**
  348.      * @param StateMachineEntity $stateMachine
  349.      * @param Context $context
  350.      */
  351.     private function uninstallStatesByStateMachine(StateMachineEntity $stateMachineContext $context): void
  352.     {
  353.         $this->deleteTransitions($stateMachine$context);
  354.         /** @var EntityRepository $repository */
  355.         $repository $this->container->get(StateMachineStateDefinition::ENTITY_NAME '.repository');
  356.         $deleteStates $this->getStates($context$stateMachinetrue);
  357.         $criteria = new Criteria();
  358.         $criteria->addFilter(new StateMachineFilter($stateMachine$deleteStates'technicalName'));
  359.         $result $repository->searchIds($criteria$context);
  360.         $delete = [];
  361.         // check every state if it is used in an order
  362.         foreach ($result->getIds() as $stateId) {
  363.             $criteria = new Criteria();
  364.             $criteria->addFilter(new EqualsFilter('stateId'$stateId));
  365.             /** @var EntityRepository $orderRepository */
  366.             $orderRepository $this->container->get(OrderDefinition::ENTITY_NAME '.repository');
  367.             $orderResult $orderRepository->searchIds($criteria$context);
  368.             if ($orderResult->getTotal() === 0) {
  369.                 $delete[] = ['id' => $stateId];
  370.             }
  371.         }
  372.         if (count($delete) > 0) {
  373.             $repository->delete(
  374.                 $delete,
  375.                 $context
  376.             );
  377.         }
  378.     }
  379.     /**
  380.      * @param Context $context
  381.      */
  382.     private function updateCustomFields(Context $context): void
  383.     {
  384.         $customFieldSetRepository $this->container->get('custom_field_set.repository');
  385.         if (!$customFieldSetRepository) {
  386.             throw new RuntimeException("Couldn't resolve service 'custom_field_set.repository'");
  387.         }
  388.         $fieldSetName VioB2BLogin::CUSTOM_FIELDSET_NAME;
  389.         $fieldSetId $customFieldSetRepository->searchIds((new Criteria())->addFilter(new EqualsFilter('name'$fieldSetName)), $context)->firstId();
  390.         if ($fieldSetId !== null) {
  391.             $this->updateCustomField(
  392.                 $fieldSetId,
  393.                 static::APPROVAL_EMPLOYEE_FIELD,
  394.                 [
  395.                     'name' => static::APPROVAL_EMPLOYEE_FIELD,
  396.                     'type' => CustomFieldTypes::ENTITY,
  397.                     'config' => [
  398.                         'label' => [
  399.                             'de-DE' => 'Freigebender Mitarbeiter',
  400.                             'en-GB' => 'Approval employee'
  401.                         ],
  402.                         'entity' => EmployeeDefinition::ENTITY_NAME,
  403.                         'componentName' => 'sw-entity-single-select',
  404.                         'labelProperty' => ['firstName''lastName']
  405.                     ],
  406.                 ],
  407.                 $context
  408.             );
  409.         }
  410.     }
  411.     /**
  412.      * @param string $fieldSetId
  413.      * @param string $fieldName
  414.      * @param array $fieldData
  415.      * @param Context $context
  416.      * @noinspection PhpSameParameterValueInspection
  417.      */
  418.     private function updateCustomField(string $fieldSetIdstring $fieldName, array $fieldDataContext $context): void
  419.     {
  420.         $customFieldRepository $this->container->get('custom_field.repository');
  421.         if (!$customFieldRepository) {
  422.             throw new RuntimeException("Couldn't resolve service 'custom_field.repository'");
  423.         }
  424.         if (!array_key_exists('name'$fieldData)) {
  425.             $fieldData['name'] = $fieldName;
  426.         }
  427.         if (!array_key_exists('customFieldSetId'$fieldData)) {
  428.             $fieldData['customFieldSetId'] = $fieldSetId;
  429.         }
  430.         $fieldId $customFieldRepository->searchIds(
  431.             (new Criteria())->addFilter(
  432.                 new EqualsFilter('name'$fieldName),
  433.                 new EqualsFilter('customFieldSetId'$fieldSetId)
  434.             ), $context)
  435.             ->firstId();
  436.         if ($fieldId !== null) {
  437.             $fieldData['id'] = $fieldId;
  438.         }
  439.         $customFieldRepository->upsert([$fieldData], $context);
  440.     }
  441.     private function removeCustomFields(Context $context): void
  442.     {
  443.         $customFieldSetRepository $this->container->get('custom_field_set.repository');
  444.         if (!$customFieldSetRepository) {
  445.             throw new RuntimeException("Couldn't resolve service 'custom_field_set.repository'");
  446.         }
  447.         $fieldSetName VioB2BLogin::CUSTOM_FIELDSET_NAME;
  448.         $fieldSetId $customFieldSetRepository->searchIds((new Criteria())->addFilter(new EqualsFilter('name'$fieldSetName)), $context)->firstId();
  449.         if ($fieldSetId !== null) {
  450.             $customFieldRepository $this->container->get('custom_field.repository');
  451.             if (!$customFieldRepository) {
  452.                 throw new RuntimeException("Couldn't resolve service 'custom_field_set.repository'");
  453.             }
  454.             $fieldId $customFieldRepository->searchIds(
  455.                 (new Criteria())->addFilter(
  456.                     new EqualsFilter('name', static::APPROVAL_EMPLOYEE_FIELD),
  457.                     new EqualsFilter('customFieldSetId'$fieldSetId)
  458.                 ), $context)
  459.                 ->firstId();
  460.             if ($fieldId) {
  461.                 $customFieldRepository->delete([
  462.                     [
  463.                         'id' => $fieldId
  464.                     ]
  465.                 ], $context);
  466.             }
  467.         }
  468.     }
  469. }