diff --git a/payment/config/install/payment.payment_method_configuration.collect_on_delivery.yml b/payment/config/install/payment.payment_method_configuration.collect_on_delivery.yml
index 458bdf3..73c00b6 100644
--- a/payment/config/install/payment.payment_method_configuration.collect_on_delivery.yml
+++ b/payment/config/install/payment.payment_method_configuration.collect_on_delivery.yml
@@ -7,5 +7,7 @@ pluginConfiguration:
   execute_status_id: payment_pending
   capture: true
   capture_status_id: payment_success
+  refund: false
+  refund_status_id: payment_refunded
 pluginId: payment_basic
 status: false
diff --git a/payment/config/install/payment.payment_method_configuration.no_payment_required.yml b/payment/config/install/payment.payment_method_configuration.no_payment_required.yml
index 96cb287..e461416 100644
--- a/payment/config/install/payment.payment_method_configuration.no_payment_required.yml
+++ b/payment/config/install/payment.payment_method_configuration.no_payment_required.yml
@@ -4,6 +4,8 @@ ownerId: 1
 pluginConfiguration:
   execute_status_id: payment_success
   capture: false
-  capture_status_id: ''
+  capture_status_id: payment_success
+  refund: true
+  refund_status_id: payment_refunded
 pluginId: payment_basic
 status: false
diff --git a/payment/css/payment.css b/payment/css/payment.css
index 8f0b50b..352870d 100644
--- a/payment/css/payment.css
+++ b/payment/css/payment.css
@@ -38,14 +38,3 @@
 }
 
 /* @end */
-
-/* @group payment_basic method configuration plugin */
-
-.payment-method-configuration-plugin-payment_basic-capture-status-id {
-    margin-left: 2em; /* LTR */
-}
-[dir="rtl"] .payment-method-configuration-plugin-payment_basic-capture-status-id {
-    margin-right: 2em;
-}
-
-/* @end */
diff --git a/payment/js/payment.js b/payment/js/payment.js
new file mode 100644
index 0000000..340d516
--- /dev/null
+++ b/payment/js/payment.js
@@ -0,0 +1,33 @@
+(function ($, Drupal, drupalSettings) {
+
+  'use strict';
+
+  Drupal.behaviors.paymentMethodConfigurationBasic = {
+    attach: function (context) {
+      var $context = $(context);
+      // Provide the vertical tab summaries.
+      $context.find('#edit-execute').drupalSetSummary(function (context) {
+        var payment_status_id = $('#edit-execute-status-id').val();
+        return Drupal.t('Sets payments to %status.', {
+          '%status' : drupalSettings.payment.payment_method_configuration_basic[payment_status_id]
+        });
+      });
+      var i;
+      var operations = ['capture', 'refund'];
+      for (i in operations) {
+        $context.find('#edit-' + operations[i]).drupalSetSummary(function (context) {
+          if ($('#' + operations[i]).is(":checked")) {
+            var payment_status_id = $('#edit-' + operations[i] + '-status-id').val();
+            return Drupal.t('Sets payments to %status.', {
+              '%status' : drupalSettings.payment.payment_method_configuration_basic[payment_status_id]
+            });
+          }
+          else {
+            return Drupal.t('Disabled.');
+          }
+        });
+      }
+    }
+  };
+
+})(jQuery, Drupal, drupalSettings);
diff --git a/payment/payment.api.php b/payment/payment.api.php
index 5e7ce8f..3991a87 100644
--- a/payment/payment.api.php
+++ b/payment/payment.api.php
@@ -180,6 +180,21 @@ function hook_payment_pre_execute(PaymentInterface $payment) {}
 function hook_payment_pre_capture(PaymentInterface $payment) {}
 
 /**
+ * Executes before a payment is refunded.
+ *
+ * @param \Drupal\payment\Entity\PaymentInterface $payment
+ *
+ * @see \Drupal\payment\Plugin\Payment\Method\PaymentMethodCapturePaymentInterface::capturePayment()
+ * @see \Drupal\payment\Event\PaymentEvents::PAYMENT_PRE_REFUND
+ * @see \Drupal\payment\Event\PaymentPreRefund
+ *
+ * @deprecated For proper dependency injection and testability, you are advised
+ * to use the \Drupal\payment\Event\PaymentEvents::PAYMENT_PRE_REFUND Symfony
+ * event instead.
+ */
+function hook_payment_pre_refund(PaymentInterface $payment) {}
+
+/**
  * Alters the IDs of payments available for referencing through an instance.
  *
  * @param string $category_id
diff --git a/payment/payment.routing.yml b/payment/payment.routing.yml
index 49d8ef3..eac30d0 100644
--- a/payment/payment.routing.yml
+++ b/payment/payment.routing.yml
@@ -52,6 +52,16 @@ payment.payment.capture:
   options:
     _admin_route: TRUE
 
+payment.payment.refund:
+  path: '/payment/{payment}/refund'
+  defaults:
+    _entity_form: 'payment.refund'
+    _title: Refund payment
+  requirements:
+    _entity_access: 'payment.refund'
+  options:
+    _admin_route: TRUE
+
 payment.payment.delete:
   path: '/payment/{payment}/delete'
   defaults:
diff --git a/payment/src/Entity/Payment.php b/payment/src/Entity/Payment.php
index ca1b63d..bb159a9 100644
--- a/payment/src/Entity/Payment.php
+++ b/payment/src/Entity/Payment.php
@@ -52,7 +52,8 @@ use Drupal\user\UserInterface;
  *     "edit-form" = "payment.payment.edit",
  *     "delete-form" = "payment.payment.delete",
  *     "update-status-form" = "payment.payment.update_status",
- *     "capture-form" = "payment.payment.capture"
+ *     "capture-form" = "payment.payment.capture",
+ *     "refund-form" = "payment.payment.refund"
  *   }
  * )
  */
diff --git a/payment/src/Entity/Payment/PaymentAccess.php b/payment/src/Entity/Payment/PaymentAccess.php
index 29e31d9..5e3b48e 100644
--- a/payment/src/Entity/Payment/PaymentAccess.php
+++ b/payment/src/Entity/Payment/PaymentAccess.php
@@ -12,6 +12,7 @@ use Drupal\Core\Entity\EntityAccessController;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\payment\Entity\PaymentInterface;
 use Drupal\payment\Plugin\Payment\Method\PaymentMethodCapturePaymentInterface;
+use Drupal\payment\Plugin\Payment\Method\PaymentMethodRefundPaymentInterface;
 use Drupal\payment\Plugin\Payment\Method\PaymentMethodUpdatePaymentStatusInterface;
 
 /**
@@ -34,8 +35,14 @@ class PaymentAccess extends EntityAccessController {
     elseif ($operation == 'capture') {
       $payment_method = $payment->getPaymentMethod();
       return $payment_method instanceof PaymentMethodCapturePaymentInterface
-        && $payment_method->capturePaymentAccess($account)
-        && $this->checkAccessPermission($payment, $operation, $account);
+      && $payment_method->capturePaymentAccess($account)
+      && $this->checkAccessPermission($payment, $operation, $account);
+    }
+    elseif ($operation == 'refund') {
+      $payment_method = $payment->getPaymentMethod();
+      return $payment_method instanceof PaymentMethodRefundPaymentInterface
+      && $payment_method->refundPaymentAccess($account)
+      && $this->checkAccessPermission($payment, $operation, $account);
     }
     return $this->checkAccessPermission($payment, $operation, $account);
   }
diff --git a/payment/src/Entity/Payment/PaymentListBuilder.php b/payment/src/Entity/Payment/PaymentListBuilder.php
index 2c198b4..70c9445 100644
--- a/payment/src/Entity/Payment/PaymentListBuilder.php
+++ b/payment/src/Entity/Payment/PaymentListBuilder.php
@@ -176,6 +176,18 @@ class PaymentListBuilder extends EntityListBuilder {
           ),
         ) + $entity->urlInfo('capture-form')->toArray();
     }
+    if ($entity->access('refund')) {
+      $operations['refund'] = array(
+          'title' => $this->t('Refund'),
+          'attributes' => array(
+            'class' => array('use-ajax'),
+            'data-accepts' => 'application/vnd.drupal-modal',
+          ),
+          'query' => array(
+            'destination' => $this->requestStack->getCurrentRequest()->attributes->get('_system_path'),
+          ),
+        ) + $entity->urlInfo('refund-form')->toArray();
+    }
 
     return $operations;
   }
diff --git a/payment/src/Entity/Payment/PaymentRefundForm.php b/payment/src/Entity/Payment/PaymentRefundForm.php
new file mode 100644
index 0000000..579d665
--- /dev/null
+++ b/payment/src/Entity/Payment/PaymentRefundForm.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\payment\Entity\Payment\PaymentRefundForm.
+ */
+
+namespace Drupal\payment\Entity\Payment;
+
+use Drupal\Core\Entity\ContentEntityConfirmFormBase;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides the payment refund form.
+ */
+class PaymentRefundForm extends ContentEntityConfirmFormBase {
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager.
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+   *    The string translator.
+   */
+  function __construct(EntityManagerInterface $entity_manager, TranslationInterface $string_translation) {
+    parent::__construct($entity_manager);
+    $this->stringTranslation = $string_translation;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static($container->get('entity.manager'), $container->get('string_translation'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return $this->t('Do you really want to refund payment #!payment_id?', array(
+      '!payment_id' => $this->getEntity()->id(),
+    ));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Refund');
+  }
+
+  /**
+   * Returns the route to go to if the user cancels the action.
+   *
+   * @return \Drupal\Core\Url
+   *   A URL object.
+   */
+  public function getCancelRoute() {
+    return $this->getEntity()->urlInfo();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submit(array $form, array &$form_state) {
+    /** @var \Drupal\payment\Entity\PaymentInterface $payment */
+    $payment = $this->getEntity();
+    /** @var \Drupal\payment\Plugin\Payment\Method\PaymentMethodRefundPaymentInterface $payment_method */
+    $payment_method = $payment->getPaymentMethod();
+    $payment_method->refundPayment();
+
+    $form_state['redirect_route'] = $this->getEntity()->urlInfo();
+  }
+
+}
diff --git a/payment/src/Event/PaymentEvents.php b/payment/src/Event/PaymentEvents.php
index e210f5d..63c2e73 100644
--- a/payment/src/Event/PaymentEvents.php
+++ b/payment/src/Event/PaymentEvents.php
@@ -38,6 +38,14 @@ final class PaymentEvents {
   const PAYMENT_PRE_CAPTURE = 'drupal.payment.payment_pre_capture';
 
   /**
+   * The name of the event that is fired before a payment is refunded.
+   *
+   * @see hook_payment_pre_refunded()
+   * @see \Drupal\payment\Event\PaymentPreRefund
+   */
+  const PAYMENT_PRE_REFUND = 'drupal.payment.payment_pre_refund';
+
+  /**
    * The name of the event that is fired after a new payment status is set.
    *
    * @see hook_payment_status_set()
diff --git a/payment/src/Event/PaymentPreRefund.php b/payment/src/Event/PaymentPreRefund.php
new file mode 100644
index 0000000..af07c9c
--- /dev/null
+++ b/payment/src/Event/PaymentPreRefund.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\payment\Event\PaymentPreRefund.
+ */
+
+namespace Drupal\payment\Event;
+
+use Drupal\payment\Entity\PaymentInterface;
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Provides an event that is dispatched before a payment is refunded.
+ *
+ * @see \Drupal\payment\Event\PaymentEvents::PAYMENT_PRE_REFUND
+ */
+class PaymentPreRefund extends Event {
+
+  /**
+   * The payment.
+   *
+   * @var \Drupal\payment\Entity\PaymentInterface
+   */
+  protected $payment;
+
+  /**-
+   * Constructs a new class instance.
+   *
+   * @param \Drupal\payment\Entity\PaymentInterface $payment
+   *   The payment that will be refunded.
+   *
+   * @param \Drupal\Core\Session\AccountInterface
+   */
+  public function __construct(PaymentInterface $payment) {
+    $this->payment = $payment;
+  }
+
+  /**
+   * Gets the payment that will be refunded.
+   *
+   * @return \Drupal\payment\Entity\PaymentInterface
+   */
+  public function getPayment() {
+    return $this->payment;
+  }
+
+}
diff --git a/payment/src/Hook/HookInfo.php b/payment/src/Hook/HookInfo.php
index 9e4dcf7..57fb4d5 100644
--- a/payment/src/Hook/HookInfo.php
+++ b/payment/src/Hook/HookInfo.php
@@ -39,6 +39,9 @@ class HookInfo {
     $hooks['payment_pre_execute'] = array(
       'group' => 'payment',
     );
+    $hooks['payment_pre_refund'] = array(
+      'group' => 'payment',
+    );
     $hooks['payment_queue_payment_ids_alter'] = array(
       'group' => 'payment',
     );
diff --git a/payment/src/Hook/Permission.php b/payment/src/Hook/Permission.php
index 571182c..04c79af 100644
--- a/payment/src/Hook/Permission.php
+++ b/payment/src/Hook/Permission.php
@@ -70,6 +70,9 @@ class Permission {
       'payment.payment.capture.any' => array(
         'title' => $this->t('Capture payments'),
       ),
+      'payment.payment.refund.any' => array(
+        'title' => $this->t('Refund payments'),
+      ),
       'payment.payment_method_configuration.update.any' => array(
         'title' => $this->t('Update any payment method configuration'),
         'restrict access' => TRUE,
diff --git a/payment/src/Plugin/Payment/Method/Basic.php b/payment/src/Plugin/Payment/Method/Basic.php
index efd97d4..aee7706 100644
--- a/payment/src/Plugin/Payment/Method/Basic.php
+++ b/payment/src/Plugin/Payment/Method/Basic.php
@@ -142,4 +142,17 @@ class Basic extends PaymentMethodBase implements ContainerFactoryPluginInterface
     return $this->getCapture() && $this->getPayment()->getStatus()->getPluginId() != $this->getCaptureStatusId();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function doRefundPayment() {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function doRefundPaymentAccess(AccountInterface $account) {
+    return FALSE;
+  }
+
 }
diff --git a/payment/src/Plugin/Payment/Method/PaymentMethodBase.php b/payment/src/Plugin/Payment/Method/PaymentMethodBase.php
index 5eb3653..99cd531 100644
--- a/payment/src/Plugin/Payment/Method/PaymentMethodBase.php
+++ b/payment/src/Plugin/Payment/Method/PaymentMethodBase.php
@@ -17,6 +17,7 @@ use Drupal\payment\Event\PaymentEvents;
 use Drupal\payment\Event\PaymentExecuteAccess;
 use Drupal\payment\Event\PaymentPreCapture;
 use Drupal\payment\Event\PaymentPreExecute;
+use Drupal\payment\Event\PaymentPreRefund;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 
@@ -29,7 +30,7 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  *   form.
  * - message_text_format: The ID of the text format to format message_text with.
  */
-abstract class PaymentMethodBase extends PluginBase implements AccessInterface, ContainerFactoryPluginInterface, PaymentMethodInterface {
+abstract class PaymentMethodBase extends PluginBase implements AccessInterface, ContainerFactoryPluginInterface, PaymentMethodInterface, PaymentMethodCapturePaymentInterface, PaymentMethodRefundPaymentInterface {
 
   /**
    * The event dispatcher.
@@ -229,7 +230,7 @@ abstract class PaymentMethodBase extends PluginBase implements AccessInterface,
   abstract protected function doExecutePayment();
 
   /**
-   * Implements \Drupal\payment\Plugin\Payment\Method\PaymentMethodCapturePaymentInterface::capturePaymentAccess().
+   * {@inheritdoc}
    */
   public function capturePaymentAccess(AccountInterface $account) {
     if (!$this->getPayment()) {
@@ -249,7 +250,7 @@ abstract class PaymentMethodBase extends PluginBase implements AccessInterface,
   abstract protected function doCapturePaymentAccess(AccountInterface $account);
 
   /**
-   * Implements \Drupal\payment\Plugin\Payment\Method\PaymentMethodCapturePaymentInterface::capturePayment().
+   * {@inheritdoc}
    */
   public function capturePayment() {
     if (!$this->getPayment()) {
@@ -268,6 +269,45 @@ abstract class PaymentMethodBase extends PluginBase implements AccessInterface,
   abstract protected function doCapturePayment();
 
   /**
+   * {@inheritdoc}
+   */
+  public function refundPaymentAccess(AccountInterface $account) {
+    if (!$this->getPayment()) {
+      throw new \LogicException('Trying to check access for a non-existing payment. A payment must be set trough self::setPayment() first.');
+    }
+
+    return $this->getPayment()->access('refund') && $this->doRefundPaymentAccess($account);
+  }
+
+  /**
+   * Performs a payment method-specific access check for payment refunds.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *
+   * @return bool
+   */
+  abstract protected function doRefundPaymentAccess(AccountInterface $account);
+
+  /**
+   * {@inheritdoc}
+   */
+  public function refundPayment() {
+    if (!$this->getPayment()) {
+      throw new \LogicException('Trying to refund a non-existing payment. A payment must be set trough self::setPayment() first.');
+    }
+    $event = new PaymentPreRefund($this->getPayment());
+    $this->eventDispatcher->dispatch(PaymentEvents::PAYMENT_PRE_REFUND, $event);
+    $this->moduleHandler->invokeAll('payment_pre_refund', array($this->getPayment()));
+    // @todo invoke Rules event.
+    $this->doRefundPayment();
+  }
+
+  /**
+   * Performs the actual payment refund.
+   */
+  abstract protected function doRefundPayment();
+
+  /**
    * Checks a payment's currency against this plugin.
    *
    * @param \Drupal\Core\Session\AccountInterface $account
diff --git a/payment/src/Plugin/Payment/Method/PaymentMethodCapturePaymentInterface.php b/payment/src/Plugin/Payment/Method/PaymentMethodCapturePaymentInterface.php
index 281d367..dea47e1 100644
--- a/payment/src/Plugin/Payment/Method/PaymentMethodCapturePaymentInterface.php
+++ b/payment/src/Plugin/Payment/Method/PaymentMethodCapturePaymentInterface.php
@@ -11,11 +11,8 @@ use Drupal\Core\Session\AccountInterface;
 /**
  * Defines a payment method that can capture authorized payments.
  *
- * Users can capture payments if they have the "payment.payment.capture.any"
- * permissions. By implementing this interface, payment methods can exercise
- * additional control on top of this permission.
- *
- * @see \Drupal\payment\Plugin\Payment\Method\PaymentMethodCapturePaymentTrait
+ * Users can refund payments if they have the "payment.payment.capture.any"
+ * permissions and self::capturePaymentAccess() returns TRUE.
  */
 interface PaymentMethodCapturePaymentInterface {
 
diff --git a/payment/src/Plugin/Payment/Method/PaymentMethodRefundPaymentInterface.php b/payment/src/Plugin/Payment/Method/PaymentMethodRefundPaymentInterface.php
new file mode 100644
index 0000000..fcae98d
--- /dev/null
+++ b/payment/src/Plugin/Payment/Method/PaymentMethodRefundPaymentInterface.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * Contains \Drupal\payment\Plugin\Payment\Method\PaymentMethodRefundPaymentInterface.
+ */
+
+namespace Drupal\payment\Plugin\Payment\Method;
+
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Defines a payment method that can Refund authorized payments.
+ *
+ * Users can refund payments if they have the "payment.payment.refund.any"
+ * permissions and self::refundPaymentAccess() returns TRUE.
+ */
+interface PaymentMethodRefundPaymentInterface {
+
+  /**
+   * Checks if the payment can be refunded.
+   *
+   * The payment method must have been configured and the payment must have been
+   * captured prior to refunding it.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *
+   * @return bool
+   *
+   * @see self::refundPayment
+   */
+  public function refundPaymentAccess(AccountInterface $account);
+
+  /**
+   * Refunds the payment.
+   *
+   * Implementations must invoke hook_payment_pre_refund() and dispatch the
+   * \Drupal\payment\Event\PaymentEvents::PAYMENT_PRE_REFUND Symfony event
+   * before refunding the payment.
+   *
+   * @see self::refundPaymentAccess
+   */
+  public function refundPayment();
+
+}
diff --git a/payment/src/Plugin/Payment/MethodConfiguration/Basic.php b/payment/src/Plugin/Payment/MethodConfiguration/Basic.php
index 38f568a..2972dfa 100644
--- a/payment/src/Plugin/Payment/MethodConfiguration/Basic.php
+++ b/payment/src/Plugin/Payment/MethodConfiguration/Basic.php
@@ -69,6 +69,8 @@ class Basic extends PaymentMethodConfigurationBase implements ContainerFactoryPl
       'execute_status_id' => 'payment_pending',
       'capture' => FALSE,
       'capture_status_id' => 'payment_success',
+      'refund' => FALSE,
+      'refund_status_id' => 'payment_refunded',
     );
   }
 
@@ -145,17 +147,101 @@ class Basic extends PaymentMethodConfigurationBase implements ContainerFactoryPl
   }
 
   /**
+   * Sets the status to set on payment refund.
+   *
+   * @param string $status
+   *   The plugin ID of the payment status to set.
+   *
+   * @return $this
+   */
+  public function setRefundStatusId($status) {
+    $this->configuration['refund_status_id'] = $status;
+
+    return $this;
+  }
+
+  /**
+   * Gets the status to set on payment refund.
+   *
+   * @return string
+   *   The plugin ID of the payment status to set.
+   */
+  public function getRefundStatusId() {
+    return $this->configuration['refund_status_id'];
+  }
+
+  /**
+   * Sets whether or not refunds are supported.
+   *
+   * @param bool $refund
+   *   Whether or not to support refunds.
+   *
+   * @return $this
+   */
+  public function setRefund($refund) {
+    $this->configuration['refund'] = $refund;
+
+    return $this;
+  }
+
+  /**
+   * Gets whether or not refunds are supported.
+   *
+   * @param bool
+   *   Whether or not to support refunds.
+   */
+  public function getRefund() {
+    return $this->configuration['refund'];
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function buildConfigurationForm(array $form, array &$form_state) {
-    $elements = parent::buildConfigurationForm($form, $form_state);
+    $form = parent::buildConfigurationForm($form, $form_state);
+    $form['plugin_form']['#process'][] = array($this, 'processBuildConfigurationForm');
+
+    return $form;
+  }
+
+  /**
+   * Implements a form API #process callback.
+   */
+  public function processBuildConfigurationForm(array &$element, array &$form_state, array &$form) {
     $elements['brand_label'] = array(
       '#default_value' => $this->getBrandLabel(),
       '#description' => $this->t('The label that payers will see when choosing a payment method. Defaults to the payment method label.'),
       '#title' => $this->t('Brand label'),
       '#type' => 'textfield',
     );
-    $elements['execute_status_id'] = array(
+    $labels = array();
+    foreach ($this->paymentStatusManager->getDefinitions() as $definition) {
+      $labels[$definition['id']] = (string) $definition['label'];
+    }
+    $elements['workflow'] = array(
+      '#attached' => array(
+        'js' => array(
+          drupal_get_path('module', 'payment') . '/js/payment.js',
+          array(
+            'data' => array(
+              'payment' => array(
+                'payment_method_configuration_basic' => $labels,
+              ),
+            ),
+            'type' => 'setting',
+          ),
+        ),
+      ),
+      '#type' => 'vertical_tabs',
+      '#title' => $this->t('Workflow'),
+    );
+    $elements['execute'] = array(
+      '#group' => 'workflow',
+      '#open' => TRUE,
+      '#type' => 'details',
+      '#title' => $this->t('Execution'),
+    );
+    $elements['execute']['execute_status_id'] = array(
       '#default_value' => !$this->getExecuteStatusId() ?: $this->getExecuteStatusId(),
       '#description' => $this->t('The status to set payments to after being executed by this payment method.'),
       '#empty_value' => '',
@@ -164,27 +250,22 @@ class Basic extends PaymentMethodConfigurationBase implements ContainerFactoryPl
       '#title' => $this->t('Payment execution status'),
       '#type' => 'select',
     );
-    $capture_id = drupal_html_id('capture');
     $elements['capture'] = array(
+      '#group' => 'workflow',
+      '#open' => TRUE,
+      '#type' => 'details',
+      '#title' => $this->t('Capture'),
+    );
+    $capture_id = drupal_html_id('capture');
+    $elements['capture']['capture'] = array(
       '#id' => $capture_id,
       '#type' => 'checkbox',
       '#title' => $this->t('Add an additional capture step after payments have been executed.'),
       '#default_value' => $this->getCapture(),
     );
-    $elements['capture_status_id_wrapper'] = array(
-      '#attributes' => array(
-        'class' => array('payment-method-configuration-plugin-payment_basic-capture-status-id'),
-      ),
-      '#type' => 'container',
-    );
-    $elements['capture_status_id_wrapper']['capture_status_id'] = array(
-      '#attached' => array(
-        'css' => array(
-          __DIR__ . '/../../../../css/payment.css',
-        ),
-      ),
+    $elements['capture']['capture_status_id'] = array(
       '#description' => $this->t('The status to set payments to after being captured by this payment method.'),
-      '#default_value' => $this->getCaptureStatusId() ? $this->getCaptureStatusId() : 'payment_success',
+      '#default_value' => $this->getCaptureStatusId(),
       '#options' => $this->paymentStatusManager->options(),
       '#required' => TRUE,
       '#states' => array(
@@ -197,6 +278,34 @@ class Basic extends PaymentMethodConfigurationBase implements ContainerFactoryPl
       '#title' => $this->t('Payment capture status'),
       '#type' => 'select',
     );
+    $refund_id = drupal_html_id('refund');
+    $elements['refund'] = array(
+      '#group' => 'workflow',
+      '#open' => TRUE,
+      '#type' => 'details',
+      '#title' => $this->t('Refund'),
+    );
+    $elements['refund']['refund'] = array(
+      '#id' => $refund_id,
+      '#type' => 'checkbox',
+      '#title' => $this->t('Add an additional refund step after payments have been executed.'),
+      '#default_value' => $this->getRefund(),
+    );
+    $elements['refund']['refund_status_id'] = array(
+      '#description' => $this->t('The status to set payments to after being refunded by this payment method.'),
+      '#default_value' => $this->getRefundStatusId(),
+      '#options' => $this->paymentStatusManager->options(),
+      '#required' => TRUE,
+      '#states' => array(
+        'visible' => array(
+          '#' . $refund_id => array(
+            'checked' => TRUE,
+          ),
+        ),
+      ),
+      '#title' => $this->t('Payment refund status'),
+      '#type' => 'select',
+    );
 
     return $elements;
   }
diff --git a/payment/src/Plugin/Payment/Status/Refunded.php b/payment/src/Plugin/Payment/Status/Refunded.php
new file mode 100644
index 0000000..5434d65
--- /dev/null
+++ b/payment/src/Plugin/Payment/Status/Refunded.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * Contains \Drupal\payment\Plugin\Payment\Status\Refunded.
+ */
+
+namespace Drupal\payment\Plugin\Payment\Status;
+
+/**
+ * A refunded payment.
+ *
+ * @PaymentStatus(
+ *   id = "payment_refunded",
+ *   label = @Translation("Refunded"),
+ *   parent_id = "payment_no_money_transferred"
+ * )
+ */
+class Failed extends PaymentStatusBase {
+}
diff --git a/payment/tests/src/Entity/Payment/PaymentAccessUnitTest.php b/payment/tests/src/Entity/Payment/PaymentAccessUnitTest.php
index 765123d..f001228 100644
--- a/payment/tests/src/Entity/Payment/PaymentAccessUnitTest.php
+++ b/payment/tests/src/Entity/Payment/PaymentAccessUnitTest.php
@@ -10,6 +10,7 @@ namespace Drupal\payment\Tests\Entity\Payment;
 use Drupal\payment\Entity\Payment\PaymentAccess;
 use Drupal\payment\Plugin\Payment\Method\PaymentMethodCapturePaymentInterface;
 use Drupal\payment\Plugin\Payment\Method\PaymentMethodInterface;
+use Drupal\payment\Plugin\Payment\Method\PaymentMethodRefundPaymentInterface;
 use Drupal\payment\Plugin\Payment\Method\PaymentMethodUpdatePaymentStatusInterface;
 use Drupal\Tests\UnitTestCase;
 
@@ -96,6 +97,56 @@ class PaymentAccessUnitTest extends UnitTestCase {
 
   /**
    * @covers ::checkAccess
+   *
+   * @dataProvider providerTestCheckAccessRefund
+   */
+  public function testCheckAccessRefund($expected, $payment_method_interface, $payment_method_refund_access, $has_permissions) {
+    $operation = 'refund';
+    $language_code = $this->randomName();
+
+    $account = $this->getMock('\Drupal\Core\Session\AccountInterface');
+    $map = array(
+      array('payment.payment.refund.any', $has_permissions),
+      array('payment.payment.refund.own', $has_permissions),
+    );
+    $account->expects($this->any())
+      ->method('hasPermission')
+      ->will($this->returnValueMap($map));
+
+    $payment_method = $this->getMock($payment_method_interface);
+    $payment_method->expects($this->any())
+      ->method('refundPaymentAccess')
+      ->with($account)
+      ->will($this->returnValue($payment_method_refund_access));
+
+    $payment = $this->getMockBuilder('\Drupal\payment\Entity\Payment')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $payment->expects($this->atLeastOnce())
+      ->method('getPaymentMethod')
+      ->will($this->returnValue($payment_method));
+
+    $method = new \ReflectionMethod($this->accessController, 'checkAccess');
+    $method->setAccessible(TRUE);
+
+    $this->assertSame($expected, $method->invokeArgs($this->accessController, array($payment, $operation, $language_code, $account)));
+  }
+
+  /**
+   * Provides data to self::testCheckAccessRefund().
+   */
+  public function providerTestCheckAccessRefund() {
+    return array(
+      array(TRUE, '\Drupal\payment\Tests\Entity\Payment\PaymentAccessUnitTestDummyPaymentMethodRefundPaymentInterface', TRUE, TRUE),
+      array(FALSE, '\Drupal\payment\Tests\Entity\Payment\PaymentAccessUnitTestDummyPaymentMethodRefundPaymentInterface', FALSE, TRUE),
+      array(FALSE, '\Drupal\payment\Tests\Entity\Payment\PaymentAccessUnitTestDummyPaymentMethodRefundPaymentInterface', TRUE, FALSE),
+      array(FALSE, '\Drupal\payment\Tests\Entity\Payment\PaymentAccessUnitTestDummyPaymentMethodRefundPaymentInterface', FALSE, FALSE),
+      array(FALSE, '\Drupal\payment\Plugin\Payment\Method\PaymentMethodInterface', TRUE, TRUE),
+    );
+  }
+
+  /**
+   * @covers ::checkAccess
    */
   public function testCheckAccessUpdateStatusWithAccess() {
     $operation = 'update_status';
@@ -276,3 +327,9 @@ interface PaymentAccessUnitTestDummyPaymentMethodUpdateStatusInterface extends P
  */
 interface PaymentAccessUnitTestDummyPaymentMethodCapturePaymentInterface extends PaymentMethodCapturePaymentInterface, PaymentMethodInterface {
 }
+
+/**
+ * Extends two interfaces, because we can only mock one.
+ */
+interface PaymentAccessUnitTestDummyPaymentMethodRefundPaymentInterface extends PaymentMethodRefundPaymentInterface, PaymentMethodInterface {
+}
diff --git a/payment/tests/src/Entity/Payment/PaymentRefundFormUnitTest.php b/payment/tests/src/Entity/Payment/PaymentRefundFormUnitTest.php
new file mode 100644
index 0000000..149e811
--- /dev/null
+++ b/payment/tests/src/Entity/Payment/PaymentRefundFormUnitTest.php
@@ -0,0 +1,150 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\payment\Tests\Entity\Payment\PaymentRefundFormUnitTest.
+ */
+
+namespace Drupal\payment\Tests\Entity\Payment;
+
+use Drupal\Core\Url;
+use Drupal\payment\Entity\Payment\PaymentRefundForm;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+  /**
+ * @coversDefaultClass \Drupal\payment\Entity\Payment\PaymentRefundForm
+ */
+class PaymentRefundFormUnitTest extends UnitTestCase {
+
+  /**
+   * The class under test.
+   *
+   * @var \Drupal\payment\Entity\Payment\PaymentRefundForm
+   */
+  protected $form;
+
+  /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $entityManager;
+
+  /**
+   * The payment.
+   *
+   * @var \Drupal\payment\Entity\Payment|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $payment;
+
+  /**
+   * The string translator.
+   *
+   * @var \Drupal\Core\StringTranslation\TranslationInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $stringTranslation;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'description' => '',
+      'group' => 'Payment',
+      'name' => '\Drupal\payment\Entity\Payment\PaymentRefundForm unit test',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @covers ::__construct
+   */
+  public function setUp() {
+    $this->entityManager = $this->getMock('\Drupal\Core\Entity\EntityManagerInterface');
+
+    $this->stringTranslation = $this->getMock('\Drupal\Core\StringTranslation\TranslationInterface');
+    $this->stringTranslation->expects($this->any())
+      ->method('translate')
+      ->will($this->returnArgument(0));
+
+    $this->payment = $this->getMockBuilder('\Drupal\payment\Entity\Payment')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $this->form = new PaymentRefundForm($this->entityManager, $this->stringTranslation);
+    $this->form->setEntity($this->payment);
+  }
+
+  /**
+   * @covers ::create
+   */
+  function testCreate() {
+    $container = $this->getMock('\Symfony\Component\DependencyInjection\ContainerInterface');
+    $map = array(
+      array('entity.manager', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, $this->entityManager),
+      array('string_translation', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, $this->stringTranslation),
+    );
+    $container->expects($this->any())
+      ->method('get')
+      ->will($this->returnValueMap($map));
+
+    $form = PaymentRefundForm::create($container);
+    $this->assertInstanceOf('\Drupal\payment\Entity\Payment\PaymentRefundForm', $form);
+  }
+
+  /**
+   * @covers ::getConfirmText
+   */
+  function testGetConfirmText() {
+    $this->assertInternalType('string', $this->form->getConfirmText());
+  }
+
+  /**
+   * @covers ::getQuestion
+   */
+  function testGetQuestion() {
+    $this->assertInternalType('string', $this->form->getQuestion());
+  }
+
+  /**
+   * @covers ::getCancelRoute
+   */
+  function testGetCancelRoute() {
+    $url = new Url($this->randomName());
+
+    $this->payment->expects($this->atLeastOnce())
+      ->method('urlInfo')
+      ->with('canonical')
+      ->will($this->returnValue($url));
+
+    $this->assertSame($url, $this->form->getCancelRoute());
+  }
+
+  /**
+   * @covers ::submit
+   */
+  function testSubmit() {
+    $payment_method = $this->getMock('\Drupal\payment\Plugin\Payment\Method\PaymentMethodRefundPaymentInterface');
+    $payment_method->expects($this->once())
+      ->method('refundPayment');
+
+    $url = new Url($this->randomName());
+
+    $this->payment->expects($this->atLeastOnce())
+      ->method('getPaymentMethod')
+      ->will($this->returnValue($payment_method));
+    $this->payment->expects($this->atLeastOnce())
+      ->method('urlInfo')
+      ->with('canonical')
+      ->will($this->returnValue($url));
+
+    $form = array();
+    $form_state = array();
+
+    $this->form->submit($form, $form_state);
+    $this->assertSame($url, $form_state['redirect_route']);
+  }
+
+}
diff --git a/payment/tests/src/Event/PaymentPreRefundUnitTest.php b/payment/tests/src/Event/PaymentPreRefundUnitTest.php
new file mode 100644
index 0000000..46a6248
--- /dev/null
+++ b/payment/tests/src/Event/PaymentPreRefundUnitTest.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\payment\Event\PaymentPreRefundUnitTest.
+ */
+
+namespace Drupal\payment\Tests\Event;
+
+use Drupal\payment\Event\PaymentPreRefund;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\payment\Event\PaymentPreRefund
+ */
+class PaymentPreRefundUnitTest extends UnitTestCase {
+
+  /**
+   * The event under test.
+   *
+   * @var \Drupal\payment\Event\PaymentPreRefund
+   */
+  protected $event;
+
+  /**
+   * The payment.
+   *
+   * @var \Drupal\payment\Entity\PaymentInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $payment;
+
+  /**
+   * {@inheritdoc}
+   */
+  static function getInfo() {
+    return array(
+      'description' => '',
+      'group' => 'Payment',
+      'name' => '\Drupal\payment\Event\PaymentPreRefund unit test',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @covers ::__construct
+   */
+  public function setUp() {
+    $this->payment = $this->getMockBuilder('\Drupal\payment\Entity\Payment')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $this->event = new PaymentPreRefund($this->payment);
+  }
+
+  /**
+   * @covers ::getPayment
+   */
+  public function testGetPayment() {
+    $this->assertSame($this->payment, $this->event->getPayment());
+  }
+
+}
diff --git a/payment/tests/src/Plugin/Payment/Method/PaymentMethodBaseUnitTest.php b/payment/tests/src/Plugin/Payment/Method/PaymentMethodBaseUnitTest.php
index 8f265c0..130f665 100644
--- a/payment/tests/src/Plugin/Payment/Method/PaymentMethodBaseUnitTest.php
+++ b/payment/tests/src/Plugin/Payment/Method/PaymentMethodBaseUnitTest.php
@@ -312,6 +312,87 @@ class PaymentMethodBaseUnitTest extends PaymentMethodBaseUnitTestBase {
   }
 
   /**
+   * @covers ::refundPayment
+   */
+  public function testRefundPayment() {
+    $payment = $this->getMockBuilder('\Drupal\payment\Entity\Payment')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $this->eventDispatcher->expects($this->once())
+      ->method('dispatch')
+      ->with(PaymentEvents::PAYMENT_PRE_REFUND, $this->isInstanceOf('\Drupal\payment\Event\PaymentPreRefund'));
+
+    $this->moduleHandler->expects($this->once())
+      ->method('invokeAll')
+      ->with('payment_pre_refund', array($payment));
+
+    $this->plugin->setPayment($payment);
+    $this->plugin->expects($this->once())
+      ->method('doRefundPayment');
+
+    $this->plugin->refundPayment();
+  }
+
+  /**
+   * @covers ::refundPayment
+   *
+   * @expectedException \Exception
+   */
+  public function testRefundPaymentWithoutPayment() {
+    $this->plugin->refundPayment();
+  }
+
+  /**
+   * @covers ::refundPaymentAccess
+   *
+   * @dataProvider providerTestRefundPaymentAccess
+   */
+  public function testRefundPaymentAccess($expected, $update_access, $do) {
+    $payment = $this->getMockBuilder('\Drupal\payment\Entity\Payment')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $payment->expects($this->once())
+      ->method('access')
+      ->with('refund')
+      ->will($this->returnValue($update_access));
+
+    $account = $this->getMock('\Drupal\Core\Session\AccountInterface');
+
+    /** @var \Drupal\payment\Plugin\Payment\Method\PaymentMethodBase|\PHPUnit_Framework_MockObject_MockObject $payment_method */
+    $this->plugin->setPayment($payment);
+    $this->plugin->expects($this->any())
+      ->method('doRefundPaymentAccess')
+      ->with($account)
+      ->will($this->returnValue($do));
+
+    $this->assertSame($expected, $this->plugin->refundPaymentAccess($account));
+  }
+
+  /**
+   * Provides data to self::testRefundPaymentAccess().
+   */
+  public function providerTestRefundPaymentAccess() {
+    return array(
+      array(TRUE, TRUE, TRUE),
+      array(FALSE, FALSE, TRUE),
+      array(FALSE, TRUE, FALSE),
+      array(FALSE, FALSE, FALSE),
+    );
+  }
+
+  /**
+   * @covers ::refundPaymentAccess
+   *
+   * @expectedException \Exception
+   */
+  public function testRefundPaymentAccessWithoutPayment() {
+    $account = $this->getMock('\Drupal\Core\Session\AccountInterface');
+
+    $this->plugin->refundPaymentAccess($account);
+  }
+
+  /**
    * @covers ::executePaymentAccessEvent
    *
    * @dataProvider providerTestExecutePaymentAccessEvent
diff --git a/payment_test/src/Plugin/Payment/Method/PaymentTest.php b/payment_test/src/Plugin/Payment/Method/PaymentTest.php
index eb88864..eea6aec 100644
--- a/payment_test/src/Plugin/Payment/Method/PaymentTest.php
+++ b/payment_test/src/Plugin/Payment/Method/PaymentTest.php
@@ -45,4 +45,17 @@ class PaymentTest extends PaymentMethodBase {
     return FALSE;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function doRefundPayment() {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function doRefundPaymentAccess(AccountInterface $account) {
+    return FALSE;
+  }
+
 }
