diff --git a/src/Plugin/Action/CreateAdjustmentAction.php b/src/Plugin/Action/CreateAdjustmentAction.php new file mode 100644 index 0000000000000000000000000000000000000000..fd86b6e8b0ef658b72a999baed2c0e1467b88242 --- /dev/null +++ b/src/Plugin/Action/CreateAdjustmentAction.php @@ -0,0 +1,234 @@ +<?php + +namespace Drupal\eca_commerce\Plugin\Action; + +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\commerce_order\Adjustment; +use Drupal\commerce_order\AdjustmentTypeManager; +use Drupal\commerce_order\EntityAdjustableInterface; +use Drupal\commerce_price\Price; +use Drupal\commerce_store\Resolver\StoreResolverInterface; +use Drupal\eca\Plugin\Action\ConfigurableActionBase; +use Drupal\eca\Plugin\ECA\PluginFormTrait; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Describes the eca_commerce add_adjustment action. + * + * This allows users to add price adjustments when added to cart based on ECA. + * + * @Action( + * id = "eca_commerce_add_adjustment", + * label = @Translation("Order Item: Add Price Adjustment"), + * eca_version_introduced = "1.0.0", + * type = "commerce_order_item" + * ) + */ +class CreateAdjustmentAction extends ConfigurableActionBase { + + use CurrencyActionTrait; + use PluginFormTrait; + + /** + * The adjustment type manager. + * + * @var \Drupal\commerce_order\AdjustmentTypeManager|null + */ + protected ?AdjustmentTypeManager $adjustmentTypeManager; + + /** + * The default store resolver. + * + * @var \Drupal\commerce_store\Resolver\StoreResolverInterface|null + */ + protected ?StoreResolverInterface $defaultStoreResolver; + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { + $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); + $instance->adjustmentTypeManager = $container->get('plugin.manager.commerce_adjustment_type', ContainerInterface::NULL_ON_INVALID_REFERENCE); + $instance->defaultStoreResolver = $container->get('commerce_store.default_store_resolver', ContainerInterface::NULL_ON_INVALID_REFERENCE); + return $instance; + } + + /** + * {@inheritdoc} + */ + public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) { + $access_result = AccessResult::AllowedIf($object instanceof EntityAdjustableInterface); + + return $return_as_object ? $access_result : $access_result->isAllowed(); + } + + /** + * {@inheritdoc} + */ + public function execute(mixed $entity = NULL): void { + if (!class_exists(Adjustment::class) || !class_exists(Price::class)) { + // Early return. + return; + } + + $label = $this->tokenService->replaceClear($this->configuration['label']); + $amount = $this->tokenService->replaceClear($this->configuration['amount']); + $fallback_currency = $this->getFallbackCurrency($entity); + $currency = $this->configuration['currency'] ?: $fallback_currency; + if ($currency === '_eca_token') { + $currency = $this->getTokenValue('currency', $fallback_currency); + } + $percentage = $this->tokenService->replaceClear($this->configuration['percentage']); + $definition = [ + 'type' => $this->configuration['type'], + 'label' => $label, + 'amount' => new Price($amount, $currency), + 'percentage' => $percentage ?: NULL, + 'source_id' => 'custom', + 'included' => $this->configuration['included'], + 'locked' => $this->configuration['locked'], + ]; + $adjustment = new Adjustment($definition); + switch ($this->configuration['method']) { + case 'set:clear': + $entity->setAdjustments([]); + + case 'append:drop_first': + $entity->addAdjustment($adjustment); + break; + } + + if ($this->configuration['save_entity']) { + $entity->save(); + } + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration(): array { + return [ + 'method' => 'set:clear', + 'type' => '_none', + 'label' => '', + 'amount' => '', + 'currency' => '', + 'percentage' => '', + 'included' => FALSE, + 'locked' => TRUE, + 'save_entity' => FALSE, + ] + parent::defaultConfiguration(); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state): array { + $form['method'] = [ + '#type' => 'select', + '#title' => $this->t('Method'), + '#default_value' => $this->configuration['method'], + '#description' => $this->t('The method to set an entity, like cleaning the old one, etc..'), + '#weight' => -40, + '#options' => [ + 'set:clear' => $this->t('Set and clear previous value'), + 'append:drop_first' => $this->t('Append and drop first when full'), + ], + ]; + $types = [ + '_none' => $this->t('- Select -'), + ]; + if (isset($this->adjustmentTypeManager)) { + foreach ($this->adjustmentTypeManager->getDefinitions() as $id => $definition) { + if (!empty($definition['has_ui'])) { + $types[$id] = $definition['label']; + } + } + } + $form['type'] = [ + '#type' => 'select', + '#title' => $this->t('Type'), + '#options' => $types, + '#weight' => 1, + '#default_value' => $this->configuration['type'], + '#required' => TRUE, + ]; + $form['locked'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Locked'), + '#description' => $this->t('Note: Adjustments added from UI interactions need to be locked to persist after an order refresh.'), + '#default_value' => $this->configuration['locked'], + ]; + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#size' => 20, + '#default_value' => $this->configuration['label'], + '#required' => TRUE, + '#eca_token_replacement' => TRUE, + ]; + $form['amount'] = [ + '#type' => 'number', + '#title' => $this->t('Amount'), + '#default_value' => $this->configuration['amount'], + '#required' => TRUE, + '#attributes' => ['class' => ['clearfix']], + '#eca_token_replacement' => TRUE, + ]; + $form['currency'] = [ + '#type' => 'select', + '#title' => $this->t('Currency'), + '#options' => ['_none' => 'Use default'] + $this->getAvailableCurrencies(), + '#default_value' => $this->configuration['currency'], + '#size' => 5, + '#required' => TRUE, + '#eca_token_select_option' => TRUE, + ]; + $form['percentage'] = [ + '#type' => 'number', + '#title' => $this->t('Percentage'), + '#default_value' => $this->configuration['percentage'], + '#attributes' => ['class' => ['clearfix']], + '#eca_token_replacement' => TRUE, + ]; + $form['included'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Included in the base price'), + '#default_value' => $this->configuration['amount'], + ]; + $form['save_entity'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Save entity'), + '#default_value' => $this->configuration['save_entity'], + '#description' => $this->t('Saves the entity or not after setting the value.'), + '#weight' => -10, + ]; + + return parent::buildConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state): void { + $this->configuration['method'] = $form_state->getValue('method'); + $this->configuration['type'] = $form_state->getValue('type'); + $this->configuration['locked'] = $form_state->getValue('locked'); + $this->configuration['label'] = $form_state->getValue('label'); + $this->configuration['amount'] = $form_state->getValue('amount'); + if ($form_state->getValue('currency') === '_none') { + $currency = ''; + } + else { + $currency = $form_state->getValue('currency'); + } + $this->configuration['currency'] = $currency; + $this->configuration['percentage'] = $form_state->getValue('percentage'); + $this->configuration['included'] = $form_state->getValue('included'); + $this->configuration['save_entity'] = $form_state->getValue('save_entity'); + parent::submitConfigurationForm($form, $form_state); + } + +} diff --git a/src/Plugin/Action/CurrencyActionTrait.php b/src/Plugin/Action/CurrencyActionTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..348751de67ac9af42f8c8669680a889ad5dd20b7 --- /dev/null +++ b/src/Plugin/Action/CurrencyActionTrait.php @@ -0,0 +1,86 @@ +<?php + +namespace Drupal\eca_commerce\Plugin\Action; + +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\commerce_order\Entity\OrderItemInterface; +use Drupal\commerce_store\Entity\EntityStoreInterface; +use Drupal\commerce_store\Entity\StoreInterface; +use Drupal\commerce_store\Resolver\StoreResolverInterface; + +/** + * Trait for resolving the default currency to use for a price adjustment. + */ +trait CurrencyActionTrait { + + /** + * The entity type manager service. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected EntityTypeManagerInterface $entityTypeManager; + + /** + * The default store resolver. + * + * @var \Drupal\commerce_store\Resolver\StoreResolverInterface|null + */ + protected ?StoreResolverInterface $defaultStoreResolver; + + /** + * Gets all available currencies configured on the site. + * + * Ensures format is compatible with currency select form element. + * + * @return array + * Array of currency codes with both keys and values populated. For example, + * @code + * ['USD' => 'USD'] + * @endcode + */ + protected function getAvailableCurrencies(): array { + $currencies = $this->entityTypeManager->getStorage('commerce_currency')->loadMultiple(); + $currency_codes = array_keys($currencies); + return array_combine($currency_codes, $currency_codes); + } + + /** + * Gets the default currency to use if the action was not configured with one. + * + * @param mixed $entity + * The entity used as a reference e.g. Order, Order Item. + * The currency of the entity's total price is used if available. + * Otherwise, the default currency of the respective Store is used. + * + * @return string + * The default currency. + */ + protected function getFallbackCurrency(mixed $entity = NULL): string { + return $entity?->getTotalPrice()?->getCurrencyCode() + ?? $this->getFallbackStore($entity)?->getDefaultCurrencyCode() + ?? 'USD'; + } + + /** + * Gets the given Order or Order Item entity's associated Store. + * + * Otherwise return the default Store. + * + * @param mixed $entity + * The entity used as a reference e.g. Order, Order Item. + * + * @return \Drupal\commerce_store\Entity\StoreInterface|null + * The respective Store. + */ + protected function getFallbackStore(mixed $entity = NULL) : ?StoreInterface { + if ($entity instanceof EntityStoreInterface) { + $store = $entity->getStore(); + } + elseif ($entity instanceof OrderItemInterface) { + $store = $entity->getOrder()?->getStore(); + } + $store ??= $this->defaultStoreResolver?->resolve(); + return $store; + } + +}