Loading commerce_sage_payments.services.yml +7 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,13 @@ services: arguments: - '@entity_type.manager' commerce_sage_payments.subscriber.payment_updater: class: Drupal\commerce_sage_payments\EventSubscriber\PaymentUpdater arguments: - '@entity_type.manager' tags: - { name: event_subscriber } logger.channel.commerce_sage_payments: class: Drupal\Core\Logger\LoggerChannel factory: logger.factory:get Loading src/EventSubscriber/PaymentUpdater.php 0 → 100644 +178 −0 Original line number Diff line number Diff line <?php namespace Drupal\commerce_sage_payments\EventSubscriber; use Drupal\commerce_transaction\Entity\TransactionInterface; use Drupal\state_machine\Event\WorkflowTransitionEvent; use Drupal\Core\Entity\EntityTypeManagerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * Updates the parent payment when a transaction is updated. */ class PaymentUpdater implements EventSubscriberInterface { /** * The entity type manager. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; /** * Constructs a new PaymentUpdater object. * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. */ public function __construct(EntityTypeManagerInterface $entity_type_manager) { $this->entityTypeManager = $entity_type_manager; } /** * {@inheritdoc} */ public static function getSubscribedEvents() { $events = [ 'commerce_transaction.settle_capture.post_transition' => ['updateForSettled', 0], 'commerce_transaction.settle_sale.post_transition' => ['updateForSettled', 0], 'commerce_transaction.settle_credit.post_transition' => ['updateForSettled', 0], 'commerce_transaction.fail_capture.post_transition' => ['updateForFailed', 0], 'commerce_transaction.fail_sale.post_transition' => ['updateForFailed', 0], 'commerce_transaction.fail_credit.post_transition' => ['updateForFailed', 0], 'commerce_transaction.decline_capture.post_transition' => ['updateForFailed', 0], 'commerce_transaction.decline_sale.post_transition' => ['updateForFailed', 0], 'commerce_transaction.decline_credit.post_transition' => ['updateForFailed', 0], 'commerce_transaction.expire_capture.post_transition' => ['updateForFailed', 0], 'commerce_transaction.expire_sale_post_transition' => ['updateForFailed', 0], 'commerce_transaction.expire_credit.post_transition' => ['updateForFailed', 0], ]; return $events; } /** * Updates the parent payment when a transaction is settled. * * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event * The transition event. */ public function updateForSettled(WorkflowTransitionEvent $event) { $this->updatePayment( $event->getEntity(), [ 'charge_state' => 'completed', 'refund_callback' => [$this, 'updateRefundedPayment'], ] ); } /** * Updates the parent payment when a transaction failed to settle. * * Remote states for failure are Error, Declined and Expired. We do not need * separate states for all cases at the payment level; we track this at the * transaction level. We simply mark the payment as `capture_failed` or * `refund_failed`. * * Store managers can see that a payment or a refund has failed and they can * either view the transaction details in Drupal (UI still to be implemented) * or in the Sage Payments Virtual Terminal. * * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event * The transition event. */ public function updateForFailed(WorkflowTransitionEvent $event) { $this->updatePayment( $event->getEntity(), [ 'charge_state' => 'capture_failed', 'refund_state' => 'refund_failed', ] ); } /** * Updates the payment for the given transaction. * * @param \Drupal\commerce_transaction\Entity\TransactionInterface $transaction * The transaction. * @param array $options * An associative array of options. For each transaction type (charge or * refund) a state or a callable must be given. Supported options are: * - charge_state (string, optional): The ID of the new state to set the * payment to when the transaction is a charge. * - charge_callback (callable, optional): The callable to call for * executing custom logic for determining and setting the new payment * state when the transaction is a charge. * - refund_state (string, optional): The ID of the new state to set the * payment to when the transaction is a refund. * - refund_callback (callable, optional): The callable to call for * executing custom logic for determining and setting the new payment * state when the transaction is a refund. */ protected function updatePayment( TransactionInterface $transaction, array $options ) { $payment_state = NULL; $type = $this->entityTypeManager ->getStorage('commerce_transaction_type') ->load($transaction->bundle()) ->getType(); if (empty($options["{$type}_callback"]) && empty($options["{$type}_state"])) { throw new \RuntimeException(sprintf( 'No state or callback given when updating the payment for transaction with ID "%s" of type "%s".', $transaction->id(), $type )); } // If we are given a callable we use that; otherwise, we must have been // given the new payment state. if ($options["{$type}_callback"]) { call_user_func($options["{$type}_callback"], $transaction); return; } $payment_state = $options["{$type}_state"]; // Update the payment entity. $payment = $transaction->getPayment(); $payment->setState($payment_state); $this->entityTypeManager ->getStorage('commerce_payment') ->save($payment); } /** * Updates the payment for settled refunds. * * @param \Drupal\commerce_transaction\Entity\TransactionInterface $transaction * The transaction. */ protected function updateRefundedPayment(TransactionInterface $transaction) { $payment = $transaction->getPayment(); $old_refunded_amount = $payment->getRefundedAmount(); $new_refunded_amount = $old_refunded_amount->add($transaction->getAmount()); // With the current implementation, we block issuing a refund from the UI if // there is already a refund pending. Therefore, there shouldn't exist other // refunds to account for when calculating the new state for the payment. It // should only be determined based on the state and the amount of the // current transaction. See // `\Drupal\commerce_sage_payments\Plugin\Commerce\PaymentGateway\Sevd::canRefundPayment()`. if ($new_refunded_amount->lessThan($payment->getAmount())) { $payment->setState('partially_refunded'); } else { $payment->setState('refunded'); } $payment->setRefundedAmount($new_refunded_amount); $this->entityTypeManager ->getStorage('commerce_payment') ->save($payment); } } src/Plugin/Commerce/PaymentType/SagePayments.php +264 −1 Original line number Diff line number Diff line Loading @@ -3,6 +3,23 @@ namespace Drupal\commerce_sage_payments\Plugin\Commerce\PaymentType; use Drupal\commerce_payment\Plugin\Commerce\PaymentType\PaymentTypeBase; use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\PaymentGatewayInterface; use Drupal\commerce_transaction\Entity\TransactionInterface; use Drupal\commerce_transaction\Entity\TransactionTypeInterface; use Drupal\commerce_transaction\MachineName\Field\Transaction as TransactionField; use Drupal\commerce_transaction\Updater\SupportsTransactionUpdatingInterface; use Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface; use Drupal\state_machine\Plugin\Workflow\WorkflowTransition; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use KrystalCode\SagePayments\Sdk\DirectApi\ClientBase; use KrystalCode\SagePayments\Sdk\DirectApi\ClientFactory; use KrystalCode\SagePayments\Sdk\DirectApi\ClientInterface; use KrystalCode\SagePayments\Sdk\DirectApi\Resource\Charges as ChargesApi; use KrystalCode\SagePayments\Sdk\DirectApi\Resource\Credits as CreditsApi; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides the payment type for Sage Payments. Loading @@ -13,7 +30,68 @@ use Drupal\commerce_payment\Plugin\Commerce\PaymentType\PaymentTypeBase; * workflow = "payment_sage_payments", * ) */ class SagePayments extends PaymentTypeBase { class SagePayments extends PaymentTypeBase implements ContainerFactoryPluginInterface, SupportsTransactionUpdatingInterface { /** * The Sage Payments Direct API client factory. * * @var \KrystalCode\SagePayments\Sdk\DirectApi\ClientFactory */ protected $clientFactory; /** * The entity type manager. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; /** * Constructs a new SagePayments object. * * @param array $configuration * A configuration array containing information about the plugin instance. * @param string $plugin_id * The plugin_id for the plugin instance. * @param mixed $plugin_definition * The plugin implementation definition. * @param \KrystalCode\SagePayments\Sdk\DirectApi\ClientFactory $client_factory * The Sage Payments Direct API client factory. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. */ public function __construct( array $configuration, $plugin_id, $plugin_definition, ClientFactory $client_factory, EntityTypeManagerInterface $entity_type_manager ) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->clientFactory = $client_factory; $this->entityTypeManager = $entity_type_manager; } /** * {@inheritdoc} */ public static function create( ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition ) { return new static( $configuration, $plugin_id, $plugin_definition, $container->get('commerce_sage_payments.api.factory'), $container->get('entity_type.manager') ); } /** * {@inheritdoc} Loading @@ -22,4 +100,189 @@ class SagePayments extends PaymentTypeBase { return []; } /** * {@inheritdoc} */ public function transactionUpdaterPendingStates(): array { return [ 'payment' => [ 'capture_pending', 'refund_pending', ], 'transaction' => [ 'capture_batch', 'sale_batch', 'credit_batch', ], ]; } /** * {@inheritdoc} */ public function updateTransaction(TransactionInterface $transaction) { $response = $this->fetchUpdate($transaction); // If the request fails the client will throw an exception. The transaction // will remain in the `Batch` state and it will be updated next time the // transaction updater runs. // // Nothing to do if the transaction is still waiting to be processed. if ($response->status === 'Batch') { return; } // Simply set the transaction to the updated state. We will be updating the // payment in an event subscriber. $field_item = $transaction->get(TransactionField::REMOTE_STATE)->first(); $field_item->applyTransition( $this->getTransactionTransition( $field_item, $response->type, $response->status ) ); $this->entityTypeManager ->getStorage('commerce_transaction') ->save($transaction); } /** * Fetches the update for the given transaction. * * @param \Drupal\commerce_transaction\Entity\TransactionInterface $transaction * The transaction. * * @return object|null * The response as an \stdClass object, or null if the response could * not be decoded. * * @throws \RuntimeException * When the transaction is of an unsupported type. */ protected function fetchUpdate(TransactionInterface $transaction) { $type = $this->entityTypeManager ->getStorage('commerce_transaction_type') ->load($transaction->bundle()) ->getType(); switch ($type) { case TransactionTypeInterface::TYPE_CHARGE: return $this ->getApiClient(ChargesApi::ID, $transaction) ->getChargesDetail($transaction->getRemoteId()); case TransactionTypeInterface::TYPE_REFUND: return $this ->getApiClient(CreditsApi::ID, $transaction) ->getCreditsDetail($transaction->getRemoteId()); default: throw new \RuntimeException(sprintf( 'Unsupported transaction type "%s", must be one of "%s" and "%s".', $type, TransactionTypeInterface::TYPE_CHARGE, TransactionTypeInterface::TYPE_REFUND )); } } /** * Returns the payment gateway plugin for the transaction's parent payment. * * @param \Drupal\commerce_transaction\Entity\TransactionInterface $transaction * The transaction. * * @return \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\PaymentGatewayInterface * The payment gateway plugin. */ protected function getTransactionGatewayPlugin( TransactionInterface $transaction ): PaymentGatewayInterface { return $transaction ->getPayment() ->getPaymentMethod() ->getPaymentGateway() ->getPlugin(); } /** * Returns the transition for the given remote state field. * * Transitions are well defined from 'Batch' to success or error states. There * is therefore only one transition for the given target state and we can * determine it programmatically to reduce code complexity i.e. avoid control * statements. * * @param \Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface $field_item * The state field item. * @param string $remote_type * The remote transaction type. * @param string $remote_status * The Sage Payment transaction status. * * @return \Drupal\state_machine\Plugin\Workflow\WorkflowTransition * The transition. * * @I Throw exception if there are more than one transition * type : bug * priority : low * label : transaction, workflow */ protected function getTransactionTransition( StateItemInterface $field_item, string $remote_type, string $remote_status ): WorkflowTransition { $to_state_id = strtolower($remote_type . '_' . $remote_status); foreach ($field_item->getTransitions() as $transition) { $to_state = $transition->getToState(); if ($to_state->getId() === $to_state_id) { return $transition; } } throw new \RuntimeException(sprintf( 'No transition found for target state "%s"', strtolower($remote_status) )); } /** * Returns the Direct API client for the given API ID. * * The client configuration is built from the transaction's parent payment * gateway plugin configuration. * * @param string $api_id * The API ID for which to build the client. * @param \Drupal\commerce_transaction\Entity\TransactionInterface $transaction * The transaction. * * @return \KrystalCode\SagePayments\Sdk\DirectApi\ClientInterface * The API client. */ protected function getApiClient( string $api_id, TransactionInterface $transaction ): ClientInterface { $gateway_plugin = $this->getTransactionGatewayPlugin($transaction); $plugin_configuration = $gateway_plugin->getConfiguration(); $client_configuration = [ 'client_id' => $plugin_configuration['credentials']['client_id'], 'client_secret' => $plugin_configuration['credentials']['client_secret'], 'merchant_id' => $plugin_configuration['credentials']['merchant_id'], 'merchant_key' => $plugin_configuration['credentials']['merchant_key'], ]; if ($gateway_plugin->getMode() === 'live') { $client_configuration['env'] = ClientBase::ENV_PRODUCTION; } return $this->clientFactory->get( $api_id, $client_configuration ); } } Loading
commerce_sage_payments.services.yml +7 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,13 @@ services: arguments: - '@entity_type.manager' commerce_sage_payments.subscriber.payment_updater: class: Drupal\commerce_sage_payments\EventSubscriber\PaymentUpdater arguments: - '@entity_type.manager' tags: - { name: event_subscriber } logger.channel.commerce_sage_payments: class: Drupal\Core\Logger\LoggerChannel factory: logger.factory:get Loading
src/EventSubscriber/PaymentUpdater.php 0 → 100644 +178 −0 Original line number Diff line number Diff line <?php namespace Drupal\commerce_sage_payments\EventSubscriber; use Drupal\commerce_transaction\Entity\TransactionInterface; use Drupal\state_machine\Event\WorkflowTransitionEvent; use Drupal\Core\Entity\EntityTypeManagerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * Updates the parent payment when a transaction is updated. */ class PaymentUpdater implements EventSubscriberInterface { /** * The entity type manager. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; /** * Constructs a new PaymentUpdater object. * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. */ public function __construct(EntityTypeManagerInterface $entity_type_manager) { $this->entityTypeManager = $entity_type_manager; } /** * {@inheritdoc} */ public static function getSubscribedEvents() { $events = [ 'commerce_transaction.settle_capture.post_transition' => ['updateForSettled', 0], 'commerce_transaction.settle_sale.post_transition' => ['updateForSettled', 0], 'commerce_transaction.settle_credit.post_transition' => ['updateForSettled', 0], 'commerce_transaction.fail_capture.post_transition' => ['updateForFailed', 0], 'commerce_transaction.fail_sale.post_transition' => ['updateForFailed', 0], 'commerce_transaction.fail_credit.post_transition' => ['updateForFailed', 0], 'commerce_transaction.decline_capture.post_transition' => ['updateForFailed', 0], 'commerce_transaction.decline_sale.post_transition' => ['updateForFailed', 0], 'commerce_transaction.decline_credit.post_transition' => ['updateForFailed', 0], 'commerce_transaction.expire_capture.post_transition' => ['updateForFailed', 0], 'commerce_transaction.expire_sale_post_transition' => ['updateForFailed', 0], 'commerce_transaction.expire_credit.post_transition' => ['updateForFailed', 0], ]; return $events; } /** * Updates the parent payment when a transaction is settled. * * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event * The transition event. */ public function updateForSettled(WorkflowTransitionEvent $event) { $this->updatePayment( $event->getEntity(), [ 'charge_state' => 'completed', 'refund_callback' => [$this, 'updateRefundedPayment'], ] ); } /** * Updates the parent payment when a transaction failed to settle. * * Remote states for failure are Error, Declined and Expired. We do not need * separate states for all cases at the payment level; we track this at the * transaction level. We simply mark the payment as `capture_failed` or * `refund_failed`. * * Store managers can see that a payment or a refund has failed and they can * either view the transaction details in Drupal (UI still to be implemented) * or in the Sage Payments Virtual Terminal. * * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event * The transition event. */ public function updateForFailed(WorkflowTransitionEvent $event) { $this->updatePayment( $event->getEntity(), [ 'charge_state' => 'capture_failed', 'refund_state' => 'refund_failed', ] ); } /** * Updates the payment for the given transaction. * * @param \Drupal\commerce_transaction\Entity\TransactionInterface $transaction * The transaction. * @param array $options * An associative array of options. For each transaction type (charge or * refund) a state or a callable must be given. Supported options are: * - charge_state (string, optional): The ID of the new state to set the * payment to when the transaction is a charge. * - charge_callback (callable, optional): The callable to call for * executing custom logic for determining and setting the new payment * state when the transaction is a charge. * - refund_state (string, optional): The ID of the new state to set the * payment to when the transaction is a refund. * - refund_callback (callable, optional): The callable to call for * executing custom logic for determining and setting the new payment * state when the transaction is a refund. */ protected function updatePayment( TransactionInterface $transaction, array $options ) { $payment_state = NULL; $type = $this->entityTypeManager ->getStorage('commerce_transaction_type') ->load($transaction->bundle()) ->getType(); if (empty($options["{$type}_callback"]) && empty($options["{$type}_state"])) { throw new \RuntimeException(sprintf( 'No state or callback given when updating the payment for transaction with ID "%s" of type "%s".', $transaction->id(), $type )); } // If we are given a callable we use that; otherwise, we must have been // given the new payment state. if ($options["{$type}_callback"]) { call_user_func($options["{$type}_callback"], $transaction); return; } $payment_state = $options["{$type}_state"]; // Update the payment entity. $payment = $transaction->getPayment(); $payment->setState($payment_state); $this->entityTypeManager ->getStorage('commerce_payment') ->save($payment); } /** * Updates the payment for settled refunds. * * @param \Drupal\commerce_transaction\Entity\TransactionInterface $transaction * The transaction. */ protected function updateRefundedPayment(TransactionInterface $transaction) { $payment = $transaction->getPayment(); $old_refunded_amount = $payment->getRefundedAmount(); $new_refunded_amount = $old_refunded_amount->add($transaction->getAmount()); // With the current implementation, we block issuing a refund from the UI if // there is already a refund pending. Therefore, there shouldn't exist other // refunds to account for when calculating the new state for the payment. It // should only be determined based on the state and the amount of the // current transaction. See // `\Drupal\commerce_sage_payments\Plugin\Commerce\PaymentGateway\Sevd::canRefundPayment()`. if ($new_refunded_amount->lessThan($payment->getAmount())) { $payment->setState('partially_refunded'); } else { $payment->setState('refunded'); } $payment->setRefundedAmount($new_refunded_amount); $this->entityTypeManager ->getStorage('commerce_payment') ->save($payment); } }
src/Plugin/Commerce/PaymentType/SagePayments.php +264 −1 Original line number Diff line number Diff line Loading @@ -3,6 +3,23 @@ namespace Drupal\commerce_sage_payments\Plugin\Commerce\PaymentType; use Drupal\commerce_payment\Plugin\Commerce\PaymentType\PaymentTypeBase; use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\PaymentGatewayInterface; use Drupal\commerce_transaction\Entity\TransactionInterface; use Drupal\commerce_transaction\Entity\TransactionTypeInterface; use Drupal\commerce_transaction\MachineName\Field\Transaction as TransactionField; use Drupal\commerce_transaction\Updater\SupportsTransactionUpdatingInterface; use Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface; use Drupal\state_machine\Plugin\Workflow\WorkflowTransition; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use KrystalCode\SagePayments\Sdk\DirectApi\ClientBase; use KrystalCode\SagePayments\Sdk\DirectApi\ClientFactory; use KrystalCode\SagePayments\Sdk\DirectApi\ClientInterface; use KrystalCode\SagePayments\Sdk\DirectApi\Resource\Charges as ChargesApi; use KrystalCode\SagePayments\Sdk\DirectApi\Resource\Credits as CreditsApi; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides the payment type for Sage Payments. Loading @@ -13,7 +30,68 @@ use Drupal\commerce_payment\Plugin\Commerce\PaymentType\PaymentTypeBase; * workflow = "payment_sage_payments", * ) */ class SagePayments extends PaymentTypeBase { class SagePayments extends PaymentTypeBase implements ContainerFactoryPluginInterface, SupportsTransactionUpdatingInterface { /** * The Sage Payments Direct API client factory. * * @var \KrystalCode\SagePayments\Sdk\DirectApi\ClientFactory */ protected $clientFactory; /** * The entity type manager. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; /** * Constructs a new SagePayments object. * * @param array $configuration * A configuration array containing information about the plugin instance. * @param string $plugin_id * The plugin_id for the plugin instance. * @param mixed $plugin_definition * The plugin implementation definition. * @param \KrystalCode\SagePayments\Sdk\DirectApi\ClientFactory $client_factory * The Sage Payments Direct API client factory. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. */ public function __construct( array $configuration, $plugin_id, $plugin_definition, ClientFactory $client_factory, EntityTypeManagerInterface $entity_type_manager ) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->clientFactory = $client_factory; $this->entityTypeManager = $entity_type_manager; } /** * {@inheritdoc} */ public static function create( ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition ) { return new static( $configuration, $plugin_id, $plugin_definition, $container->get('commerce_sage_payments.api.factory'), $container->get('entity_type.manager') ); } /** * {@inheritdoc} Loading @@ -22,4 +100,189 @@ class SagePayments extends PaymentTypeBase { return []; } /** * {@inheritdoc} */ public function transactionUpdaterPendingStates(): array { return [ 'payment' => [ 'capture_pending', 'refund_pending', ], 'transaction' => [ 'capture_batch', 'sale_batch', 'credit_batch', ], ]; } /** * {@inheritdoc} */ public function updateTransaction(TransactionInterface $transaction) { $response = $this->fetchUpdate($transaction); // If the request fails the client will throw an exception. The transaction // will remain in the `Batch` state and it will be updated next time the // transaction updater runs. // // Nothing to do if the transaction is still waiting to be processed. if ($response->status === 'Batch') { return; } // Simply set the transaction to the updated state. We will be updating the // payment in an event subscriber. $field_item = $transaction->get(TransactionField::REMOTE_STATE)->first(); $field_item->applyTransition( $this->getTransactionTransition( $field_item, $response->type, $response->status ) ); $this->entityTypeManager ->getStorage('commerce_transaction') ->save($transaction); } /** * Fetches the update for the given transaction. * * @param \Drupal\commerce_transaction\Entity\TransactionInterface $transaction * The transaction. * * @return object|null * The response as an \stdClass object, or null if the response could * not be decoded. * * @throws \RuntimeException * When the transaction is of an unsupported type. */ protected function fetchUpdate(TransactionInterface $transaction) { $type = $this->entityTypeManager ->getStorage('commerce_transaction_type') ->load($transaction->bundle()) ->getType(); switch ($type) { case TransactionTypeInterface::TYPE_CHARGE: return $this ->getApiClient(ChargesApi::ID, $transaction) ->getChargesDetail($transaction->getRemoteId()); case TransactionTypeInterface::TYPE_REFUND: return $this ->getApiClient(CreditsApi::ID, $transaction) ->getCreditsDetail($transaction->getRemoteId()); default: throw new \RuntimeException(sprintf( 'Unsupported transaction type "%s", must be one of "%s" and "%s".', $type, TransactionTypeInterface::TYPE_CHARGE, TransactionTypeInterface::TYPE_REFUND )); } } /** * Returns the payment gateway plugin for the transaction's parent payment. * * @param \Drupal\commerce_transaction\Entity\TransactionInterface $transaction * The transaction. * * @return \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\PaymentGatewayInterface * The payment gateway plugin. */ protected function getTransactionGatewayPlugin( TransactionInterface $transaction ): PaymentGatewayInterface { return $transaction ->getPayment() ->getPaymentMethod() ->getPaymentGateway() ->getPlugin(); } /** * Returns the transition for the given remote state field. * * Transitions are well defined from 'Batch' to success or error states. There * is therefore only one transition for the given target state and we can * determine it programmatically to reduce code complexity i.e. avoid control * statements. * * @param \Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface $field_item * The state field item. * @param string $remote_type * The remote transaction type. * @param string $remote_status * The Sage Payment transaction status. * * @return \Drupal\state_machine\Plugin\Workflow\WorkflowTransition * The transition. * * @I Throw exception if there are more than one transition * type : bug * priority : low * label : transaction, workflow */ protected function getTransactionTransition( StateItemInterface $field_item, string $remote_type, string $remote_status ): WorkflowTransition { $to_state_id = strtolower($remote_type . '_' . $remote_status); foreach ($field_item->getTransitions() as $transition) { $to_state = $transition->getToState(); if ($to_state->getId() === $to_state_id) { return $transition; } } throw new \RuntimeException(sprintf( 'No transition found for target state "%s"', strtolower($remote_status) )); } /** * Returns the Direct API client for the given API ID. * * The client configuration is built from the transaction's parent payment * gateway plugin configuration. * * @param string $api_id * The API ID for which to build the client. * @param \Drupal\commerce_transaction\Entity\TransactionInterface $transaction * The transaction. * * @return \KrystalCode\SagePayments\Sdk\DirectApi\ClientInterface * The API client. */ protected function getApiClient( string $api_id, TransactionInterface $transaction ): ClientInterface { $gateway_plugin = $this->getTransactionGatewayPlugin($transaction); $plugin_configuration = $gateway_plugin->getConfiguration(); $client_configuration = [ 'client_id' => $plugin_configuration['credentials']['client_id'], 'client_secret' => $plugin_configuration['credentials']['client_secret'], 'merchant_id' => $plugin_configuration['credentials']['merchant_id'], 'merchant_key' => $plugin_configuration['credentials']['merchant_key'], ]; if ($gateway_plugin->getMode() === 'live') { $client_configuration['env'] = ClientBase::ENV_PRODUCTION; } return $this->clientFactory->get( $api_id, $client_configuration ); } }