From b14615f12a5ef7c4812bd9b0aa210cc28256ae45 Mon Sep 17 00:00:00 2001
From: tavi toporjinschi <vasike@gmail.com>
Date: Wed, 15 Mar 2017 13:16:42 +0000
Subject: [PATCH 1/9] Issue #2828525 by vasike, bojanz: Implement manual
 payment gateways - Commit 1.

---
 modules/payment/commerce_payment.module            |   2 +
 modules/payment/commerce_payment.workflows.yml     |  45 ++++
 .../config/schema/commerce_payment.schema.yml      |  14 ++
 modules/payment/src/PaymentMethodStorage.php       |   5 +-
 .../Commerce/CheckoutPane/PaymentInstructions.php  |  87 ++++++++
 .../Commerce/CheckoutPane/PaymentProcess.php       |  20 ++
 .../HasPaymentInstructionsInterface.php            |  19 ++
 .../src/Plugin/Commerce/PaymentGateway/Manual.php  | 230 +++++++++++++++++++++
 .../PaymentGateway/ManualPaymentGatewayBase.php    |  10 +
 .../ManualPaymentGatewayInterface.php              |  29 +++
 .../Commerce/PaymentGateway/PaymentGatewayBase.php |  51 ++++-
 .../SupportsManualWorkflowInterface.php            |  41 ++++
 .../Plugin/Commerce/PaymentMethodType/Manual.php   |  54 +++++
 .../Plugin/Commerce/PaymentType/PaymentManual.php  |  23 +++
 .../payment/src/PluginForm/PaymentCancelForm.php   |  40 ++++
 .../payment/src/PluginForm/PaymentCompleteForm.php |  23 +++
 .../src/PluginForm/PaymentMethodAddForm.php        |  27 +++
 .../templates/commerce-payment-method.html.twig    |   2 +-
 .../src/Plugin/Commerce/PaymentGateway/Onsite.php  |   2 +-
 19 files changed, 712 insertions(+), 12 deletions(-)
 create mode 100644 modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInstructions.php
 create mode 100644 modules/payment/src/Plugin/Commerce/PaymentGateway/HasPaymentInstructionsInterface.php
 create mode 100644 modules/payment/src/Plugin/Commerce/PaymentGateway/Manual.php
 create mode 100644 modules/payment/src/Plugin/Commerce/PaymentGateway/ManualPaymentGatewayBase.php
 create mode 100644 modules/payment/src/Plugin/Commerce/PaymentGateway/ManualPaymentGatewayInterface.php
 create mode 100644 modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsManualWorkflowInterface.php
 create mode 100644 modules/payment/src/Plugin/Commerce/PaymentMethodType/Manual.php
 create mode 100644 modules/payment/src/Plugin/Commerce/PaymentType/PaymentManual.php
 create mode 100644 modules/payment/src/PluginForm/PaymentCancelForm.php
 create mode 100644 modules/payment/src/PluginForm/PaymentCompleteForm.php

diff --git a/modules/payment/commerce_payment.module b/modules/payment/commerce_payment.module
index e1db90f..09c5dc0 100755
--- a/modules/payment/commerce_payment.module
+++ b/modules/payment/commerce_payment.module
@@ -93,6 +93,8 @@ function template_preprocess_commerce_payment_method(array &$variables) {
       '#markup' => $payment_method->label(),
     ],
   ];
+  $expires = $payment_method->getExpiresTime();
+  $variables['payment_method_expires'] = $expires ? date('n/Y', $expires) : t('Never');
   foreach (Element::children($variables['elements']) as $key) {
     $variables['payment_method'][$key] = $variables['elements'][$key];
   }
diff --git a/modules/payment/commerce_payment.workflows.yml b/modules/payment/commerce_payment.workflows.yml
index f7d8fb4..f9be349 100644
--- a/modules/payment/commerce_payment.workflows.yml
+++ b/modules/payment/commerce_payment.workflows.yml
@@ -48,3 +48,48 @@ payment_default:
       label: 'Refund payment'
       from: [capture_completed, capture_partially_refunded]
       to: capture_refunded
+
+payment_manual:
+  id: payment_manual
+  group: commerce_payment
+  label: 'Manual'
+  states:
+    new:
+      label: 'New'
+    pending:
+      label: 'Pending'
+    completed:
+      label: 'Completed'
+    refunded:
+      label: 'Refunded'
+    partially_refunded:
+      label: 'Partially refunded'
+    expired:
+      label: 'Expired'
+    canceled:
+      label: 'Canceled'
+  transitions:
+    pending_payment:
+      label: 'Pending payment'
+      from: [new]
+      to: pending
+    cancel:
+      label: 'Cancel payment'
+      from: [pending]
+      to: canceled
+    expire:
+      label: 'Expire payment'
+      from: [pending]
+      to: expired
+    payment_received:
+      label: 'Payment received'
+      from: [pending]
+      to: completed
+    partially_refund:
+      label: 'Partially refund payment'
+      from: [completed]
+      to: partially_refunded
+    refund:
+      label: 'Refund payment'
+      from: [completed, partially_refunded]
+      to: refunded
diff --git a/modules/payment/config/schema/commerce_payment.schema.yml b/modules/payment/config/schema/commerce_payment.schema.yml
index 24881a5..d1dedb6 100644
--- a/modules/payment/config/schema/commerce_payment.schema.yml
+++ b/modules/payment/config/schema/commerce_payment.schema.yml
@@ -20,6 +20,20 @@ commerce_payment.commerce_payment_gateway.*:
 commerce_payment.commerce_payment_gateway.plugin.*:
   type: commerce_payment_gateway_configuration
 
+commerce_payment.commerce_payment_gateway.plugin.manual:
+  type: commerce_payment_gateway_configuration
+  mapping:
+    reusable:
+      type: boolean
+      label: 'Reusable'
+    expires:
+      type: string
+      label: 'Expires'
+    instructions:
+      type: text_format
+      label: 'Instructions'
+      translatable: true
+
 commerce_payment_gateway_configuration:
   type: mapping
   mapping:
diff --git a/modules/payment/src/PaymentMethodStorage.php b/modules/payment/src/PaymentMethodStorage.php
index 6d63002..3c52350 100644
--- a/modules/payment/src/PaymentMethodStorage.php
+++ b/modules/payment/src/PaymentMethodStorage.php
@@ -82,8 +82,11 @@ public function loadReusable(UserInterface $account, PaymentGatewayInterface $pa
     $query = $this->getQuery()
       ->condition('uid', $account->id())
       ->condition('payment_gateway', $payment_gateway->id())
-      ->condition('reusable', TRUE)
+      ->condition('reusable', TRUE);
+    $group = $query->orConditionGroup()
       ->condition('expires', $this->time->getRequestTime(), '>')
+      ->condition('expires', 0);
+    $query->condition($group)
       ->sort('created', 'DESC');
     $result = $query->execute();
     if (empty($result)) {
diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInstructions.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInstructions.php
new file mode 100644
index 0000000..1d0d558
--- /dev/null
+++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInstructions.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Drupal\commerce_payment\Plugin\Commerce\CheckoutPane;
+
+use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface;
+use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneBase;
+use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\ManualPaymentGatewayInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides the payment instructions pane.
+ *
+ * @CommerceCheckoutPane(
+ *   id = "payment_instructions",
+ *   label = @Translation("Payment instructions"),
+ *   default_step = "complete",
+ *   wrapper_element = "fieldset",
+ * )
+ */
+class PaymentInstructions extends CheckoutPaneBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a new PaymentInstructions 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 \Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface $checkout_flow
+   *   The parent checkout flow.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow, EntityTypeManagerInterface $entity_type_manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $checkout_flow);
+
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow = NULL) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $checkout_flow,
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) {
+    // The payment gateway is currently always required to be set.
+    if ($this->order->get('payment_gateway')->isEmpty()) {
+      return [];
+    }
+
+    /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */
+    $payment_gateway = $this->order->payment_gateway->entity;
+    /** @var \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\PaymentGatewayInterface $payment_gateway_plugin */
+    $payment_gateway_plugin = $payment_gateway->getPlugin();
+
+    if ($payment_gateway_plugin instanceof ManualPaymentGatewayInterface && $payment_gateway_plugin->getPaymentInstructions()) {
+      $pane_form += $payment_gateway_plugin->getPaymentInstructions();
+      return $pane_form;
+    }
+
+    return [];
+  }
+
+}
diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php
index 9fdb900..ba739b0 100644
--- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php
+++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php
@@ -7,6 +7,7 @@
 use Drupal\commerce_order\Entity\OrderInterface;
 use Drupal\commerce_payment\Exception\DeclineException;
 use Drupal\commerce_payment\Exception\PaymentGatewayException;
+use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\ManualPaymentGatewayInterface;
 use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsitePaymentGatewayInterface;
 use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OnsitePaymentGatewayInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
@@ -145,6 +146,7 @@ public function buildPaneForm(array $pane_form, FormStateInterface $form_state,
     $payment_gateway_plugin = $payment_gateway->getPlugin();
 
     $payment_storage = $this->entityTypeManager->getStorage('commerce_payment');
+    /** @var \Drupal\commerce_payment\Entity\Payment $payment */
     $payment = $payment_storage->create([
       'state' => 'new',
       'amount' => $this->order->getTotalPrice(),
@@ -186,6 +188,24 @@ public function buildPaneForm(array $pane_form, FormStateInterface $form_state,
 
       return $pane_form;
     }
+    elseif ($payment_gateway_plugin instanceof ManualPaymentGatewayInterface) {
+      try {
+        $payment->payment_method = $this->order->payment_method->entity;
+        $payment_gateway_plugin->createPayment($payment);
+        $this->checkoutFlow->redirectToStep($this->checkoutFlow->getNextStepId());
+      }
+      catch (DeclineException $e) {
+        $message = $this->t('We encountered an error processing your payment method. Please verify your details and try again.');
+        drupal_set_message($message, 'error');
+        $this->redirectToPreviousStep();
+      }
+      catch (PaymentGatewayException $e) {
+        \Drupal::logger('commerce_payment')->error($e->getMessage());
+        $message = $this->t('We encountered an unexpected error processing your payment method. Please try again later.');
+        drupal_set_message($message, 'error');
+        $this->redirectToPreviousStep();
+      }
+    }
     else {
       $this->checkoutFlow->redirectToStep($this->checkoutFlow->getNextStepId());
     }
diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/HasPaymentInstructionsInterface.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/HasPaymentInstructionsInterface.php
new file mode 100644
index 0000000..a645cea
--- /dev/null
+++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/HasPaymentInstructionsInterface.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Drupal\commerce_payment\Plugin\Commerce\PaymentGateway;
+
+/**
+ * Defines the interface for gateways which support payment methods with
+ * instructions.
+ */
+interface HasPaymentInstructionsInterface {
+
+  /**
+   * Creates a payment method with the given payment instructions.
+   *
+   * @return array|NULL
+   *   A renderable array containing payment instructions or NULL.
+   */
+  public function getPaymentInstructions();
+
+}
diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/Manual.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/Manual.php
new file mode 100644
index 0000000..c0b31f2
--- /dev/null
+++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/Manual.php
@@ -0,0 +1,230 @@
+<?php
+
+namespace Drupal\commerce_payment\Plugin\Commerce\PaymentGateway;
+
+use Drupal\commerce_payment\Entity\PaymentInterface;
+use Drupal\commerce_payment\Entity\PaymentMethodInterface;
+use Drupal\commerce_payment\Exception\HardDeclineException;
+use Drupal\commerce_payment\Exception\InvalidRequestException;
+use Drupal\commerce_price\Price;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides the Manual payment gateway.
+ *
+ * @CommercePaymentGateway(
+ *   id = "manual",
+ *   label = "Manual",
+ *   display_label = "Manual",
+ *   payment_type = "payment_manual",
+ *   payment_method_types = {"manual"},
+ *   modes = {"manual" = "Manual"},
+ * )
+ */
+class Manual extends ManualPaymentGatewayBase implements ManualPaymentGatewayInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'reusable' => FALSE,
+      'expires' => '',
+      'instructions' => [
+        'value' => '',
+        'format' => 'plain_text',
+      ],
+    ] + parent::defaultConfiguration();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form = parent::buildConfigurationForm($form, $form_state);
+
+    $form['manual'] = [
+      '#type' => 'fieldset',
+      '#title' => t('Manual payment settings'),
+    ];
+    $form['manual']['reusable'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Reusable'),
+      '#description' => $this->t('Check if you want to have reusable payment methods for this gateway.'),
+      '#default_value' => $this->configuration['reusable'],
+    ];
+    $form['manual']['expires'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Expires'),
+      '#description' => $this->t('An offset from the current time such as "@example1", "@example2 or "@example3". Leave empty for never expires.', ['@example1' => '1 year', '@example2' => '3 months', '@example3' => '60 days']),
+      '#default_value' => $this->configuration['expires'],
+      '#size' => 10,
+    ];
+    $form['manual']['instructions'] = [
+      '#type' => 'text_format',
+      '#title' => $this->t('Instructions'),
+      '#description' => $this->t('Manual payment instructions to be displayed to customer on checkout.'),
+      '#default_value' => $this->configuration['instructions']['value'],
+      '#format' => $this->configuration['instructions']['format'],
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    parent::validateConfigurationForm($form, $form_state);
+
+    if (!$form_state->getErrors()) {
+      $form_parents = $form['#parents'];
+      $form_parents[] = 'manual';
+      $values = $form_state->getValue($form_parents);
+      if (!empty($values['expires'])) {
+        $convert = strtotime($values['expires']);
+        if ($convert == -1 || $convert === FALSE) {
+          $form_state->setError($form['manual']['expires'], $this->t('Invalid offset time format.'));
+        }
+        if ($convert < REQUEST_TIME) {
+          $form_state->setError($form['manual']['expires'], $this->t('Future offset time is needed for Expires.'));
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    parent::submitConfigurationForm($form, $form_state);
+
+    if (!$form_state->getErrors()) {
+      $form_parents = $form['#parents'];
+      $form_parents[] = 'manual';
+      $values = $form_state->getValue($form_parents);
+      $this->configuration['instructions'] = $values['instructions'];
+      $this->configuration['reusable'] = $values['reusable'];
+      $this->configuration['expires'] = $values['expires'];
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createPayment(PaymentInterface $payment, $capture = TRUE) {
+    if ($payment->getState()->value != 'new') {
+      throw new \InvalidArgumentException('The provided payment is in an invalid state.');
+    }
+    $payment_method = $payment->getPaymentMethod();
+    if (empty($payment_method)) {
+      throw new \InvalidArgumentException('The provided payment has no payment method referenced.');
+    }
+    if ($payment_method->isExpired()) {
+      throw new HardDeclineException('The provided payment method has expired');
+    }
+
+    $payment->state = 'pending';
+    $payment->setAuthorizedTime(REQUEST_TIME);
+    $payment->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function completePayment(PaymentInterface $payment, Price $amount = NULL) {
+    if ($payment->getState()->value != 'pending') {
+      throw new \InvalidArgumentException('Only payments in the "authorization" state can be captured.');
+    }
+
+    // If not specified, capture the entire amount.
+    $amount = $amount ?: $payment->getAmount();
+
+    $payment->state = 'completed';
+    $payment->setAmount($amount);
+    $payment->setCapturedTime(REQUEST_TIME);
+    $payment->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function cancelPayment(PaymentInterface $payment) {
+    if ($payment->getState()->value != 'pending') {
+      throw new \InvalidArgumentException('Only payments in the "authorization" state can be voided.');
+    }
+
+    $payment->state = 'canceled';
+    $payment->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function refundPayment(PaymentInterface $payment, Price $amount = NULL) {
+    if (!in_array($payment->getState()->value, ['completed', 'partially_refunded'])) {
+      throw new \InvalidArgumentException('Only payments in the "completed" and "partially_refunded" states can be refunded.');
+    }
+    // If not specified, refund the entire amount.
+    $amount = $amount ?: $payment->getAmount();
+
+    // Validate the requested amount.
+    $balance = $payment->getBalance();
+    if ($amount->greaterThan($balance)) {
+      throw new InvalidRequestException(sprintf("Can't refund more than %s.", $balance->__toString()));
+    }
+
+    $old_refunded_amount = $payment->getRefundedAmount();
+    $new_refunded_amount = $old_refunded_amount->add($amount);
+    if ($new_refunded_amount->lessThan($payment->getAmount())) {
+      $payment->state = 'partially_refunded';
+    }
+    else {
+      $payment->state = 'refunded';
+    }
+
+    $payment->setRefundedAmount($new_refunded_amount);
+    $payment->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createPaymentMethod(PaymentMethodInterface $payment_method, array $payment_details) {
+    // No expected keys required for Manual payments.
+
+    // Set expires according with configuration.
+    $expires = $this->configuration['expires'] ? strtotime($this->configuration['expires']) : 0;
+    // The remote ID returned by the request.
+    $remote_id = $payment_method->getOwnerId();
+
+    $payment_method->setRemoteId($remote_id);
+    $payment_method->setReusable($this->configuration['reusable']);
+    $payment_method->setExpiresTime($expires);
+    $payment_method->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deletePaymentMethod(PaymentMethodInterface $payment_method) {
+    // Delete the local entity.
+    $payment_method->delete();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPaymentInstructions() {
+    $element = NULL;
+    $instructions = $this->configuration['instructions'];
+    if (!empty($instructions['value'])) {
+      $element['instructions'] = [
+        '#markup' => check_markup($instructions['value'], $instructions['format']),
+      ];
+    }
+
+    return $element;
+  }
+
+}
diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/ManualPaymentGatewayBase.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/ManualPaymentGatewayBase.php
new file mode 100644
index 0000000..f8a21ca
--- /dev/null
+++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/ManualPaymentGatewayBase.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Drupal\commerce_payment\Plugin\Commerce\PaymentGateway;
+
+/**
+ * Provides the base class for manual payment gateways.
+ */
+abstract class ManualPaymentGatewayBase extends PaymentGatewayBase implements ManualPaymentGatewayInterface {
+
+}
diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/ManualPaymentGatewayInterface.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/ManualPaymentGatewayInterface.php
new file mode 100644
index 0000000..7e2c72a
--- /dev/null
+++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/ManualPaymentGatewayInterface.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Drupal\commerce_payment\Plugin\Commerce\PaymentGateway;
+
+use Drupal\commerce_payment\Entity\PaymentInterface;
+
+/**
+ * Provides the interface for the manual payment gateway.
+ *
+ * The ManualPaymentGatewayInterface is the base interface which all on-site
+ * gateways implement. The other interfaces signal which additional capabilities
+ * the gateway has. The gateway plugin is free to expose additional methods,
+ * which would be defined below.
+ */
+interface ManualPaymentGatewayInterface extends PaymentGatewayInterface, HasPaymentInstructionsInterface, SupportsManualWorkflowInterface, SupportsRefundsInterface, SupportsStoredPaymentMethodsInterface {
+
+  /**
+   * Creates a payment.
+   *
+   * @param \Drupal\commerce_payment\Entity\PaymentInterface $payment
+   *   The payment.
+   *
+   * @throws \InvalidArgumentException
+   *   If $capture is FALSE but the plugin does not support authorizations.
+   * @throws \Drupal\commerce_payment\Exception\PaymentGatewayException
+   *   Thrown when the transaction fails for any reason.
+   */
+  public function createPayment(PaymentInterface $payment);
+}
diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/PaymentGatewayBase.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/PaymentGatewayBase.php
index 7ee6d3f..66beff1 100644
--- a/modules/payment/src/Plugin/Commerce/PaymentGateway/PaymentGatewayBase.php
+++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/PaymentGatewayBase.php
@@ -114,6 +114,11 @@ protected function getDefaultForms() {
       $default_forms['capture-payment'] = 'Drupal\commerce_payment\PluginForm\PaymentCaptureForm';
       $default_forms['void-payment'] = 'Drupal\commerce_payment\PluginForm\PaymentVoidForm';
     }
+    if ($this instanceof SupportsManualWorkflowInterface) {
+      $default_forms['manual-payment-method'] = 'Drupal\commerce_payment\PluginForm\PaymentMethodManualForm';
+      $default_forms['complete-payment'] = 'Drupal\commerce_payment\PluginForm\PaymentCompleteForm';
+      $default_forms['cancel-payment'] = 'Drupal\commerce_payment\PluginForm\PaymentCancelForm';
+    }
     if ($this instanceof SupportsRefundsInterface) {
       $default_forms['refund-payment'] = 'Drupal\commerce_payment\PluginForm\PaymentRefundForm';
     }
@@ -230,14 +235,27 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       return $payment_method_type->getLabel();
     }, $this->paymentMethodTypes);
 
-    $form['mode'] = [
-      '#type' => 'radios',
-      '#title' => $this->t('Mode'),
-      '#options' => $modes,
-      '#default_value' => $this->configuration['mode'],
-      '#required' => TRUE,
-      '#access' => !empty($modes),
-    ];
+    if (count($modes) > 1) {
+      // Ajax sometimes mixes up with modes.
+      if (!in_array($this->configuration['mode'], array_keys($modes))) {
+        $this->configuration = $this->defaultConfiguration();
+      }
+      $form['mode'] = [
+        '#type' => 'radios',
+        '#title' => $this->t('Mode'),
+        '#options' => $modes,
+        '#default_value' => $this->configuration['mode'],
+        '#required' => TRUE,
+        '#access' => !empty($modes),
+      ];
+    }
+    else {
+      $form['mode'] = [
+        '#type' => 'value',
+        '#value' => $this->configuration['mode'],
+      ];
+    }
+
     if (count($payment_method_types) > 1) {
       $form['payment_method_types'] = [
         '#type' => 'checkboxes',
@@ -266,7 +284,7 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
    * {@inheritdoc}
    */
   public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
-    if (!$form_state->getErrors()) {
+    if (!$form_state->getErrors() && $form_state->isSubmitted()) {
       $values = $form_state->getValue($form['#parents']);
       $values['payment_method_types'] = array_filter($values['payment_method_types']);
 
@@ -295,6 +313,21 @@ public function buildPaymentOperations(PaymentInterface $payment) {
         'access' => $access,
       ];
     }
+    if ($this instanceof SupportsManualWorkflowInterface) {
+      $access = $payment->getState()->value == 'pending';
+      $operations['complete'] = [
+        'title' => $this->t('Complete'),
+        'page_title' => $this->t('Complete payment'),
+        'plugin_form' => 'complete-payment',
+        'access' => $access,
+      ];
+      $operations['cancel'] = [
+        'title' => $this->t('Cancel payment'),
+        'page_title' => $this->t('Cancel payment'),
+        'plugin_form' => 'cancel-payment',
+        'access' => $access,
+      ];
+    }
     if ($this instanceof SupportsRefundsInterface) {
       $access = in_array($payment->getState()->value, ['capture_completed', 'capture_partially_refunded']);
       $operations['refund'] = [
diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsManualWorkflowInterface.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsManualWorkflowInterface.php
new file mode 100644
index 0000000..4355351
--- /dev/null
+++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsManualWorkflowInterface.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\commerce_payment\Plugin\Commerce\PaymentGateway;
+
+use Drupal\commerce_payment\Entity\PaymentInterface;
+use Drupal\commerce_price\Price;
+
+/**
+ * Defines the interface for gateways which support manual payment workflow.
+ */
+interface SupportsManualWorkflowInterface {
+
+  /**
+   * Completes the given pending payment.
+   *
+   * Only payments in the 'authorization' state can be captured.
+   *
+   * @param \Drupal\commerce_payment\Entity\PaymentInterface $payment
+   *   The payment to capture.
+   * @param \Drupal\commerce_price\Price $amount
+   *   The amount to capture. If NULL, defaults to the entire payment amount.
+   *
+   * @throws \Drupal\commerce_payment\Exception\PaymentGatewayException
+   *   Thrown when the transaction fails for any reason.
+   */
+  public function completePayment(PaymentInterface $payment, Price $amount = NULL);
+
+  /**
+   * Voids the given payment.
+   *
+   * Only payments in the 'pending' state can be voided.
+   *
+   * @param \Drupal\commerce_payment\Entity\PaymentInterface $payment
+   *   The payment to cancel.
+   *
+   * @throws \Drupal\commerce_payment\Exception\PaymentGatewayException
+   *   Thrown when the transaction fails for any reason.
+   */
+  public function cancelPayment(PaymentInterface $payment);
+
+}
diff --git a/modules/payment/src/Plugin/Commerce/PaymentMethodType/Manual.php b/modules/payment/src/Plugin/Commerce/PaymentMethodType/Manual.php
new file mode 100644
index 0000000..b782793
--- /dev/null
+++ b/modules/payment/src/Plugin/Commerce/PaymentMethodType/Manual.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\commerce_payment\Plugin\Commerce\PaymentMethodType;
+
+use Drupal\commerce_payment\Entity\PaymentMethodInterface;
+
+/**
+ * Provides the manual payment method type.
+ *
+ * @CommercePaymentMethodType(
+ *   id = "manual",
+ *   label = @Translation("manual"),
+ *   create_label = @Translation("New manual"),
+ * )
+ */
+class Manual extends PaymentMethodTypeBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildLabel(PaymentMethodInterface $payment_method) {
+    $payment_gateway = $payment_method->getPaymentGateway();
+    // Use billing profile address data to identify the payment method.
+    if ($billing_profile = $payment_method->getBillingProfile()) {
+      /** @var \Drupal\address\AddressInterface $address */
+      $address = $billing_profile->address->first();
+      $name = $address->getGivenName() . ' ' . $address->getFamilyName();
+      $location = $address->getAddressLine1() . ', ' . $address->getLocality();
+      $args = [
+        '@gateway_title' => $payment_gateway->label(),
+        '@name' => $name,
+        '@location' => $location,
+      ];
+      $label = $this->t('@gateway_title for @name (@location)', $args);
+    }
+    else {
+      $args = [
+        '@gateway_title' => $payment_gateway->label(),
+      ];
+      $label = $this->t('Manual - @gateway_title', $args);
+    }
+
+    return $label;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildFieldDefinitions() {
+    // Probably the fields for the Offline payments should be done in the UI.
+    return [];
+  }
+
+}
diff --git a/modules/payment/src/Plugin/Commerce/PaymentType/PaymentManual.php b/modules/payment/src/Plugin/Commerce/PaymentType/PaymentManual.php
new file mode 100644
index 0000000..ee4d28c
--- /dev/null
+++ b/modules/payment/src/Plugin/Commerce/PaymentType/PaymentManual.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\commerce_payment\Plugin\Commerce\PaymentType;
+
+/**
+ * Provides the manual payment type.
+ *
+ * @CommercePaymentType(
+ *   id = "payment_manual",
+ *   label = @Translation("Manual"),
+ *   workflow = "payment_manual",
+ * )
+ */
+class PaymentManual extends PaymentTypeBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildFieldDefinitions() {
+    return [];
+  }
+
+}
diff --git a/modules/payment/src/PluginForm/PaymentCancelForm.php b/modules/payment/src/PluginForm/PaymentCancelForm.php
new file mode 100644
index 0000000..e87a526
--- /dev/null
+++ b/modules/payment/src/PluginForm/PaymentCancelForm.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\commerce_payment\PluginForm;
+
+use Drupal\Core\Form\FormStateInterface;
+
+class PaymentCancelForm extends PaymentGatewayFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    /** @var \Drupal\commerce_payment\Entity\PaymentInterface $payment */
+    $payment = $this->entity;
+
+    $form['#theme'] = 'confirm_form';
+    $form['#attributes']['class'][] = 'confirmation';
+    $form['#page_title'] = t('Are you sure you want to cancel the %label payment?', [
+      '%label' => $payment->label(),
+    ]);
+    $form['#success_message'] = t('Payment canceled.');
+    $form['description'] = [
+      '#markup' => t('This action cannot be undone.'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    /** @var \Drupal\commerce_payment\Entity\PaymentInterface $payment */
+    $payment = $this->entity;
+    /** @var \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsManualWorkflowInterface $payment_gateway_plugin */
+    $payment_gateway_plugin = $this->plugin;
+    $payment_gateway_plugin->cancelPayment($payment);
+  }
+
+}
diff --git a/modules/payment/src/PluginForm/PaymentCompleteForm.php b/modules/payment/src/PluginForm/PaymentCompleteForm.php
new file mode 100644
index 0000000..efd848b
--- /dev/null
+++ b/modules/payment/src/PluginForm/PaymentCompleteForm.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\commerce_payment\PluginForm;
+
+use Drupal\commerce_price\Price;
+use Drupal\Core\Form\FormStateInterface;
+
+class PaymentCompleteForm extends PaymentCaptureForm {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $values = $form_state->getValue($form['#parents']);
+    $amount = new Price($values['amount']['number'], $values['amount']['currency_code']);
+    /** @var \Drupal\commerce_payment\Entity\PaymentInterface $payment */
+    $payment = $this->entity;
+    /** @var \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsManualWorkflowInterface $payment_gateway_plugin */
+    $payment_gateway_plugin = $this->plugin;
+    $payment_gateway_plugin->completePayment($payment, $amount);
+  }
+
+}
diff --git a/modules/payment/src/PluginForm/PaymentMethodAddForm.php b/modules/payment/src/PluginForm/PaymentMethodAddForm.php
index 0a3ece9..64dd9b2 100644
--- a/modules/payment/src/PluginForm/PaymentMethodAddForm.php
+++ b/modules/payment/src/PluginForm/PaymentMethodAddForm.php
@@ -51,6 +51,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
     elseif ($payment_method->bundle() == 'paypal') {
       $form['payment_details'] = $this->buildPayPalForm($form['payment_details'], $form_state);
     }
+    elseif ($payment_method->bundle() == 'manual') {
+      $form['payment_details'] = $this->buildManualForm($form['payment_details'], $form_state);
+    }
 
     /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */
     $payment_method = $this->entity;
@@ -252,6 +255,30 @@ protected function submitCreditCardForm(array $element, FormStateInterface $form
   }
 
   /**
+   * Builds the Manual form.
+   *
+   * Empty by default because there is no generic Manual form, it's always
+   * payment gateway specific.
+   *
+   * @param array $element
+   *   The target element.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the complete form.
+   *
+   * @return array
+   *   The built manual form.
+   */
+  protected function buildManualForm(array $element, FormStateInterface $form_state) {
+    // Placeholder for the PayPal mail.
+    $element['manual'] = [
+      '#type' => 'hidden',
+      '#value' => '',
+    ];
+
+    return $element;
+  }
+
+  /**
    * Builds the PayPal form.
    *
    * Empty by default because there is no generic PayPal form, it's always
diff --git a/modules/payment/templates/commerce-payment-method.html.twig b/modules/payment/templates/commerce-payment-method.html.twig
index 43283be..d165b49 100644
--- a/modules/payment/templates/commerce-payment-method.html.twig
+++ b/modules/payment/templates/commerce-payment-method.html.twig
@@ -24,7 +24,7 @@
     {{ payment_method.label }}
   </div>
   <div class="field field--name-expires">
-    {{ 'Expires'|t }} {{ payment_method_entity.expiresTime|format_date('custom', 'n/Y') }}
+    {{ 'Expires'|t }} {{ payment_method_expires }}
   </div>
   {{ payment_method.billing_profile }}
 </article>
diff --git a/modules/payment_example/src/Plugin/Commerce/PaymentGateway/Onsite.php b/modules/payment_example/src/Plugin/Commerce/PaymentGateway/Onsite.php
index 9c22df1..a953e57 100644
--- a/modules/payment_example/src/Plugin/Commerce/PaymentGateway/Onsite.php
+++ b/modules/payment_example/src/Plugin/Commerce/PaymentGateway/Onsite.php
@@ -92,7 +92,7 @@ public function createPayment(PaymentInterface $payment, $capture = TRUE) {
     if (empty($payment_method)) {
       throw new \InvalidArgumentException('The provided payment has no payment method referenced.');
     }
-    if (REQUEST_TIME >= $payment_method->getExpiresTime()) {
+    if ($payment_method->isExpired()) {
       throw new HardDeclineException('The provided payment method has expired');
     }
 

From 2e248c4a80af861f74db34bfa39bbf551177974a Mon Sep 17 00:00:00 2001
From: tavi toporjinschi <vasike@gmail.com>
Date: Wed, 15 Mar 2017 14:39:33 +0000
Subject: [PATCH 2/9] Issue #2828525 by vasike, bojanz: Implement manual
 payment gateways - Commit 2 - some travis errors and pr request changes.

---
 modules/payment/src/Entity/PaymentMethod.php                       | 2 +-
 .../Commerce/PaymentGateway/HasPaymentInstructionsInterface.php    | 2 +-
 modules/payment/src/Plugin/Commerce/PaymentGateway/Manual.php      | 7 +++----
 .../Commerce/PaymentGateway/ManualPaymentGatewayInterface.php      | 1 +
 4 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/modules/payment/src/Entity/PaymentMethod.php b/modules/payment/src/Entity/PaymentMethod.php
index 3036346..39d9770 100644
--- a/modules/payment/src/Entity/PaymentMethod.php
+++ b/modules/payment/src/Entity/PaymentMethod.php
@@ -188,7 +188,7 @@ public function setDefault($default) {
    */
   public function isExpired() {
     $expires = $this->getExpiresTime();
-    return $expires > 0 && $expires <= REQUEST_TIME;
+    return $expires > 0 && $expires <= \Drupal::service('commerce.time')->getRequestTime();
   }
 
   /**
diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/HasPaymentInstructionsInterface.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/HasPaymentInstructionsInterface.php
index a645cea..439acee 100644
--- a/modules/payment/src/Plugin/Commerce/PaymentGateway/HasPaymentInstructionsInterface.php
+++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/HasPaymentInstructionsInterface.php
@@ -11,7 +11,7 @@
   /**
    * Creates a payment method with the given payment instructions.
    *
-   * @return array|NULL
+   * @return array|null
    *   A renderable array containing payment instructions or NULL.
    */
   public function getPaymentInstructions();
diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/Manual.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/Manual.php
index c0b31f2..9a756d9 100644
--- a/modules/payment/src/Plugin/Commerce/PaymentGateway/Manual.php
+++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/Manual.php
@@ -86,7 +86,7 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
         if ($convert == -1 || $convert === FALSE) {
           $form_state->setError($form['manual']['expires'], $this->t('Invalid offset time format.'));
         }
-        if ($convert < REQUEST_TIME) {
+        if ($convert < \Drupal::service('commerce.time')->getRequestTime()) {
           $form_state->setError($form['manual']['expires'], $this->t('Future offset time is needed for Expires.'));
         }
       }
@@ -125,7 +125,7 @@ public function createPayment(PaymentInterface $payment, $capture = TRUE) {
     }
 
     $payment->state = 'pending';
-    $payment->setAuthorizedTime(REQUEST_TIME);
+    $payment->setAuthorizedTime(\Drupal::service('commerce.time')->getRequestTime());
     $payment->save();
   }
 
@@ -142,7 +142,7 @@ public function completePayment(PaymentInterface $payment, Price $amount = NULL)
 
     $payment->state = 'completed';
     $payment->setAmount($amount);
-    $payment->setCapturedTime(REQUEST_TIME);
+    $payment->setCapturedTime(\Drupal::service('commerce.time')->getRequestTime());
     $payment->save();
   }
 
@@ -192,7 +192,6 @@ public function refundPayment(PaymentInterface $payment, Price $amount = NULL) {
    */
   public function createPaymentMethod(PaymentMethodInterface $payment_method, array $payment_details) {
     // No expected keys required for Manual payments.
-
     // Set expires according with configuration.
     $expires = $this->configuration['expires'] ? strtotime($this->configuration['expires']) : 0;
     // The remote ID returned by the request.
diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/ManualPaymentGatewayInterface.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/ManualPaymentGatewayInterface.php
index 7e2c72a..cd0f654 100644
--- a/modules/payment/src/Plugin/Commerce/PaymentGateway/ManualPaymentGatewayInterface.php
+++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/ManualPaymentGatewayInterface.php
@@ -26,4 +26,5 @@
    *   Thrown when the transaction fails for any reason.
    */
   public function createPayment(PaymentInterface $payment);
+
 }

From 486dafe9745757c6dbad0396c4ac45b9ee2ebea9 Mon Sep 17 00:00:00 2001
From: tavi toporjinschi <vasike@gmail.com>
Date: Fri, 17 Mar 2017 07:30:29 +0000
Subject: [PATCH 3/9] Fix for checkout complete - commerce latest updates.

---
 modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php
index a7dc0b8..5856daa 100644
--- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php
+++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php
@@ -193,7 +193,7 @@ public function buildPaneForm(array $pane_form, FormStateInterface $form_state,
       try {
         $payment->payment_method = $this->order->payment_method->entity;
         $payment_gateway_plugin->createPayment($payment);
-        $this->checkoutFlow->redirectToStep($this->checkoutFlow->getNextStepId());
+        $this->checkoutFlow->redirectToStep($next_step_id);
       }
       catch (DeclineException $e) {
         $message = $this->t('We encountered an error processing your payment method. Please verify your details and try again.');

From a10d064c93930772bb9b158924ffabb02600ea66 Mon Sep 17 00:00:00 2001
From: tavi toporjinschi <vasike@gmail.com>
Date: Fri, 17 Mar 2017 07:31:49 +0000
Subject: [PATCH 4/9] Give custom manual mode - back to text/live modes.

---
 modules/payment/src/Plugin/Commerce/PaymentGateway/Manual.php | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/Manual.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/Manual.php
index 9a756d9..3f6d0b9 100644
--- a/modules/payment/src/Plugin/Commerce/PaymentGateway/Manual.php
+++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/Manual.php
@@ -18,7 +18,6 @@
  *   display_label = "Manual",
  *   payment_type = "payment_manual",
  *   payment_method_types = {"manual"},
- *   modes = {"manual" = "Manual"},
  * )
  */
 class Manual extends ManualPaymentGatewayBase implements ManualPaymentGatewayInterface {
@@ -124,6 +123,8 @@ public function createPayment(PaymentInterface $payment, $capture = TRUE) {
       throw new HardDeclineException('The provided payment method has expired');
     }
 
+    $test = $this->getMode() == 'test';
+    $payment->setTest($test);
     $payment->state = 'pending';
     $payment->setAuthorizedTime(\Drupal::service('commerce.time')->getRequestTime());
     $payment->save();

From db45ec172e92569f2967615c4bb14bcbd2cd1945 Mon Sep 17 00:00:00 2001
From: tavi toporjinschi <vasike@gmail.com>
Date: Fri, 17 Mar 2017 07:32:58 +0000
Subject: [PATCH 5/9] Fix Refund access for manual payments.

---
 .../payment/src/Plugin/Commerce/PaymentGateway/PaymentGatewayBase.php   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/PaymentGatewayBase.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/PaymentGatewayBase.php
index 66beff1..6c31d33 100644
--- a/modules/payment/src/Plugin/Commerce/PaymentGateway/PaymentGatewayBase.php
+++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/PaymentGatewayBase.php
@@ -329,7 +329,7 @@ public function buildPaymentOperations(PaymentInterface $payment) {
       ];
     }
     if ($this instanceof SupportsRefundsInterface) {
-      $access = in_array($payment->getState()->value, ['capture_completed', 'capture_partially_refunded']);
+      $access = in_array($payment->getState()->value, ['capture_completed', 'capture_partially_refunded', 'completed']);
       $operations['refund'] = [
         'title' => $this->t('Refund'),
         'page_title' => $this->t('Refund payment'),

From 41b12293429c588c3a0642144b260708675ab272 Mon Sep 17 00:00:00 2001
From: tavi toporjinschi <vasike@gmail.com>
Date: Fri, 17 Mar 2017 07:37:31 +0000
Subject: [PATCH 6/9] Add tests for Manual payments.

---
 .../src/Functional/ManualPaymentAdminTest.php      | 258 ++++++++++++++++++++
 .../src/Functional/ManualPaymentMethodTest.php     | 124 ++++++++++
 .../ManualPaymentCheckoutTest.php                  | 261 +++++++++++++++++++++
 3 files changed, 643 insertions(+)
 create mode 100644 modules/payment/tests/src/Functional/ManualPaymentAdminTest.php
 create mode 100644 modules/payment/tests/src/Functional/ManualPaymentMethodTest.php
 create mode 100644 modules/payment/tests/src/FunctionalJavascript/ManualPaymentCheckoutTest.php

diff --git a/modules/payment/tests/src/Functional/ManualPaymentAdminTest.php b/modules/payment/tests/src/Functional/ManualPaymentAdminTest.php
new file mode 100644
index 0000000..4dadd12
--- /dev/null
+++ b/modules/payment/tests/src/Functional/ManualPaymentAdminTest.php
@@ -0,0 +1,258 @@
+<?php
+
+namespace Drupal\Tests\commerce_payment\Functional;
+
+use Drupal\commerce_payment\Entity\Payment;
+use Drupal\commerce_price\Price;
+use Drupal\Tests\commerce\Functional\CommerceBrowserTestBase;
+
+/**
+ * Tests the admin payment UI for Manual payments.
+ *
+ * @group commerce
+ */
+class ManualPaymentAdminTest extends CommerceBrowserTestBase {
+
+  /**
+   * An on-site payment gateway.
+   *
+   * @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface
+   */
+  protected $paymentGateway;
+
+  /**
+   * Admin's payment method.
+   *
+   * @var \Drupal\commerce_payment\Entity\PaymentMethodInterface
+   */
+  protected $paymentMethod;
+
+  /**
+   * The base admin payment uri.
+   *
+   * @var string
+   */
+  protected $paymentUri;
+
+  /**
+   * The admin's order.
+   *
+   * @var \Drupal\commerce_order\Entity\OrderInterface
+   */
+  protected $order;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'commerce_order',
+    'commerce_product',
+    'commerce_payment',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getAdministratorPermissions() {
+    return array_merge([
+      'administer commerce_order',
+      'administer commerce_payment_gateway',
+      'administer commerce_payment',
+    ], parent::getAdministratorPermissions());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $profile = $this->createEntity('profile', [
+      'type' => 'customer',
+      'address' => [
+        'country_code' => 'US',
+        'postal_code' => '53177',
+        'locality' => 'Milwaukee',
+        'address_line1' => 'Pabst Blue Ribbon Dr',
+        'administrative_area' => 'WI',
+        'given_name' => 'Frederick',
+        'family_name' => 'Pabst',
+      ],
+      'uid' => $this->loggedInUser->id(),
+    ]);
+
+    $this->paymentGateway = $this->createEntity('commerce_payment_gateway', [
+      'id' => 'manual',
+      'label' => 'Manual example',
+      'plugin' => 'manual',
+    ]);
+    $this->paymentGateway->getPlugin()->setConfiguration([
+      'reusable' => '1',
+      'expires' => '',
+      'instructions' => [
+        'value' => 'Test instructions.',
+        'format' => 'plain_text'
+      ],
+    ]);
+    $this->paymentGateway->save();
+    $this->paymentMethod = $this->createEntity('commerce_payment_method', [
+      'uid' => $this->loggedInUser->id(),
+      'type' => 'manual',
+      'payment_gateway' => 'manual',
+      'billing_profile' => $profile,
+      'expires' => 0,
+    ]);
+
+    $details = ['manual' => ''];
+    $this->paymentGateway->getPlugin()->createPaymentMethod($this->paymentMethod, $details);
+
+    $variation = $this->createEntity('commerce_product_variation', [
+      'type' => 'default',
+      'sku' => 'test-product-01',
+      'price' => new Price('10', 'USD'),
+    ]);
+
+    $order_item = $this->createEntity('commerce_order_item', [
+      'type' => 'default',
+      'quantity' => 1,
+      'purchased_entity' => $variation,
+      'unit_price' => new Price('10', 'USD'),
+    ]);
+
+    $this->order = $this->createEntity('commerce_order', [
+      'uid' => $this->loggedInUser->id(),
+      'type' => 'default',
+      'state' => 'draft',
+      'order_items' => [$order_item],
+      'store_id' => $this->store,
+    ]);
+
+    $this->paymentUri = 'admin/commerce/orders/' . $this->order->id() . '/payments';
+  }
+
+  /**
+   * Tests creating a payment for an order.
+   */
+  public function testPaymentCreation() {
+    $this->drupalGet($this->paymentUri);
+    $this->getSession()->getPage()->clickLink('Add payment');
+    $this->assertSession()->addressEquals($this->paymentUri . '/add');
+    $this->assertSession()->pageTextContains('Manual example for Frederick Pabst (Pabst Blue Ribbon Dr, Milwaukee)');
+
+    $this->assertSession()->checkboxChecked('payment_method');
+
+    $this->getSession()->getPage()->pressButton('Continue');
+    $this->submitForm(['payment[amount][number]' => '100'], 'Add payment');
+    $this->assertSession()->addressEquals($this->paymentUri);
+    $this->assertSession()->pageTextContains('Pending');
+
+    /** @var \Drupal\commerce_payment\Entity\PaymentInterface $payment */
+    $payment = Payment::load(1);
+    $this->assertEquals($payment->getOrderId(), $this->order->id());
+    $this->assertEquals($payment->getAmount()->getNumber(), '100');
+  }
+
+  /**
+   * Tests completing a payment after creation.
+   */
+  public function testPaymentComplete() {
+    $payment = $this->createEntity('commerce_payment', [
+      'payment_gateway' => $this->paymentGateway->id(),
+      'payment_method' => $this->paymentMethod->id(),
+      'order_id' => $this->order->id(),
+      'amount' => new Price('10', 'USD'),
+    ]);
+
+    $this->paymentGateway->getPlugin()->createPayment($payment);
+
+    $this->drupalGet($this->paymentUri);
+    $this->assertSession()->pageTextContains('Pending');
+
+    $this->drupalGet($this->paymentUri . '/' . $payment->id() . '/operation/complete');
+    $this->submitForm(['payment[amount][number]' => '10'], 'Complete');
+    $this->assertSession()->addressEquals($this->paymentUri);
+    $this->assertSession()->pageTextNotContains('Pending');
+    $this->assertSession()->pageTextContains('Completed');
+
+    $payment = Payment::load($payment->id());
+    $this->assertEquals($payment->getState()->getLabel(), 'Completed');
+  }
+
+  /**
+   * Tests refunding a payment after completing.
+   */
+  public function testPaymentRefund() {
+    $payment = $this->createEntity('commerce_payment', [
+      'payment_gateway' => $this->paymentGateway->id(),
+      'payment_method' => $this->paymentMethod->id(),
+      'order_id' => $this->order->id(),
+      'amount' => new Price('10', 'USD'),
+    ]);
+
+    $this->paymentGateway->getPlugin()->createPayment($payment);
+    $this->paymentGateway->getPlugin()->completePayment($payment, new Price('10', 'USD'));
+
+    $this->drupalGet($this->paymentUri);
+    $this->assertSession()->pageTextContains('Completed');
+
+    $this->drupalGet($this->paymentUri . '/' . $payment->id() . '/operation/refund');
+    $this->submitForm(['payment[amount][number]' => '10'], 'Refund');
+    $this->assertSession()->addressEquals($this->paymentUri);
+    $this->assertSession()->pageTextNotContains('Completed');
+    $this->assertSession()->pageTextContains('Refunded');
+
+    $payment = Payment::load($payment->id());
+    $this->assertEquals($payment->getState()->getLabel(), 'Refunded');
+  }
+
+  /**
+   * Tests canceling a payment after creation.
+   */
+  public function testPaymentCancel() {
+    $payment = $this->createEntity('commerce_payment', [
+      'payment_gateway' => $this->paymentGateway->id(),
+      'payment_method' => $this->paymentMethod->id(),
+      'order_id' => $this->order->id(),
+      'amount' => new Price('10', 'USD'),
+    ]);
+
+    $this->paymentGateway->getPlugin()->createPayment($payment);
+
+    $this->drupalGet($this->paymentUri);
+    $this->assertSession()->pageTextContains('Pending');
+
+    $this->drupalGet($this->paymentUri . '/' . $payment->id() . '/operation/cancel');
+    $this->getSession()->getPage()->pressButton('Cancel payment');
+    $this->assertSession()->addressEquals($this->paymentUri);
+    $this->assertSession()->pageTextContains('Canceled');
+
+    $payment = Payment::load($payment->id());
+    $this->assertEquals($payment->getState()->getLabel(), 'Canceled');
+  }
+
+  /**
+   * Tests deleting a payment after creation.
+   */
+  public function testPaymentDelete() {
+    $payment = $this->createEntity('commerce_payment', [
+      'payment_gateway' => $this->paymentGateway->id(),
+      'payment_method' => $this->paymentMethod->id(),
+      'order_id' => $this->order->id(),
+      'amount' => new Price('10', 'USD'),
+    ]);
+
+    $this->paymentGateway->getPlugin()->createPayment($payment);
+
+    $this->drupalGet($this->paymentUri);
+    $this->assertSession()->pageTextContains('Pending');
+
+    $this->drupalGet($this->paymentUri . '/' . $payment->id() . '/delete');
+    $this->getSession()->getPage()->pressButton('Delete');
+    $this->assertSession()->addressEquals($this->paymentUri);
+    $this->assertSession()->pageTextNotContains('Pending');
+
+    $payment = Payment::load($payment->id());
+    $this->assertNull($payment);
+  }
+
+}
diff --git a/modules/payment/tests/src/Functional/ManualPaymentMethodTest.php b/modules/payment/tests/src/Functional/ManualPaymentMethodTest.php
new file mode 100644
index 0000000..5ffb112
--- /dev/null
+++ b/modules/payment/tests/src/Functional/ManualPaymentMethodTest.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace Drupal\Tests\commerce_payment\Functional;
+
+use Drupal\commerce_payment\Entity\PaymentMethod;
+use Drupal\Tests\commerce\Functional\CommerceBrowserTestBase;
+
+/**
+ * Tests the payment method UI for Manual type.
+ *
+ * @group commerce
+ */
+class ManualPaymentMethodTest extends CommerceBrowserTestBase {
+
+  /**
+   * A normal user with minimum permissions.
+   *
+   * @var \Drupal\User\UserInterface
+   */
+  protected $user;
+
+  /**
+   * The payment method collection url.
+   *
+   * @var string
+   */
+  protected $collectionUrl;
+
+  /**
+   * An on-site payment gateway.
+   *
+   * @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface
+   */
+  protected $paymentGateway;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'commerce_payment',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $permissions = [
+      'manage own commerce_payment_method',
+    ];
+    $this->user = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($this->user);
+
+    $this->collectionUrl = 'user/' . $this->user->id() . '/payment-methods';
+
+    /** @var \Drupal\commerce_payment\Entity\PaymentGateway $payment_gateway */
+    $this->paymentGateway = $this->createEntity('commerce_payment_gateway', [
+      'id' => 'example_manual',
+      'label' => 'Example Manual',
+      'plugin' => 'manual',
+    ]);
+    $this->paymentGateway->getPlugin()->setConfiguration([
+      'reusable' => '1',
+      'expires' => '',
+      'instructions' => [
+        'value' => 'Test instructions.',
+        'format' => 'plain_text'
+      ],
+      'payment_method_types' => ['manual'],
+    ]);
+    $this->paymentGateway->save();
+  }
+
+  /**
+   * Tests creating a payment method.
+   */
+  public function testPaymentMethodCreation() {
+    /** @var \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\ManualPaymentGatewayInterface $plugin */
+    $this->drupalGet($this->collectionUrl);
+    $this->getSession()->getPage()->clickLink('Add payment method');
+    $this->assertSession()->addressEquals($this->collectionUrl . '/add');
+
+    $form_values = [
+      'payment_method[billing_information][address][0][address][given_name]' => 'Johnny',
+      'payment_method[billing_information][address][0][address][family_name]' => 'Appleseed',
+      'payment_method[billing_information][address][0][address][address_line1]' => '123 New York Drive',
+      'payment_method[billing_information][address][0][address][locality]' => 'New York City',
+      'payment_method[billing_information][address][0][address][administrative_area]' => 'NY',
+      'payment_method[billing_information][address][0][address][postal_code]' => '10001',
+    ];
+    $this->submitForm($form_values, 'Save');
+    $this->assertSession()->addressEquals($this->collectionUrl);
+    $this->assertSession()->pageTextContains('Manual for Johnny Appleseed (123 New York Drive, New York City)');
+
+    $payment_method = PaymentMethod::load(1);
+    $this->assertEquals($this->user->id(), $payment_method->getOwnerId());
+  }
+
+  /**
+   * Tests deleting a payment method.
+   */
+  public function testPaymentMethodDeletion() {
+    $payment_method = $this->createEntity('commerce_payment_method', [
+      'uid' => $this->user->id(),
+      'type' => 'manual',
+      'payment_gateway' => 'example_manual',
+    ]);
+
+    $details = [];
+    $this->paymentGateway->getPlugin()->createPaymentMethod($payment_method, $details);
+    $this->paymentGateway->save();
+
+    $this->drupalGet($this->collectionUrl . '/' . $payment_method->id() . '/delete');
+
+    $this->assertSession()->pageTextContains('Manual - Example Manual');
+    $this->getSession()->getPage()->pressButton('Delete');
+    $this->assertSession()->addressEquals($this->collectionUrl);
+
+    $payment_gateway = PaymentMethod::load($payment_method->id());
+    $this->assertNull($payment_gateway);
+  }
+
+}
diff --git a/modules/payment/tests/src/FunctionalJavascript/ManualPaymentCheckoutTest.php b/modules/payment/tests/src/FunctionalJavascript/ManualPaymentCheckoutTest.php
new file mode 100644
index 0000000..67fbc0c
--- /dev/null
+++ b/modules/payment/tests/src/FunctionalJavascript/ManualPaymentCheckoutTest.php
@@ -0,0 +1,261 @@
+<?php
+
+namespace Drupal\Tests\commerce_payment\FunctionalJavascript;
+
+use Drupal\commerce_order\Entity\Order;
+use Drupal\commerce_payment\Entity\Payment;
+use Drupal\commerce_payment\Entity\PaymentGateway;
+use Drupal\commerce_payment\Entity\PaymentMethod;
+use Drupal\Tests\commerce\Functional\CommerceBrowserTestBase;
+use Drupal\Tests\commerce\FunctionalJavascript\JavascriptTestTrait;
+
+/**
+ * Tests the integration between payments and checkout.
+ *
+ * @group commerce
+ */
+class ManualPaymentCheckoutTest extends CommerceBrowserTestBase {
+
+  use JavascriptTestTrait;
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $account;
+
+  /**
+   * The product.
+   *
+   * @var \Drupal\commerce_product\Entity\ProductInterface
+   */
+  protected $product;
+
+  /**
+   * A non-reusable order payment method.
+   *
+   * @var \Drupal\commerce_payment\Entity\PaymentMethodInterface
+   */
+  protected $orderPaymentMethod;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'commerce_product',
+    'commerce_cart',
+    'commerce_checkout',
+    'commerce_payment',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getAdministratorPermissions() {
+    return array_merge([
+      'administer profiles',
+    ], parent::getAdministratorPermissions());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $variation = $this->createEntity('commerce_product_variation', [
+      'type' => 'default',
+      'sku' => strtolower($this->randomMachineName()),
+      'price' => [
+        'number' => '39.99',
+        'currency_code' => 'USD',
+      ],
+    ]);
+
+    /** @var \Drupal\commerce_product\Entity\ProductInterface $product */
+    $this->product = $this->createEntity('commerce_product', [
+      'type' => 'default',
+      'title' => 'My product',
+      'variations' => [$variation],
+      'stores' => [$this->store],
+    ]);
+
+    /** @var \Drupal\commerce_payment\Entity\PaymentGateway $gateway */
+    $gateway = PaymentGateway::create([
+      'id' => 'manual',
+      'label' => 'Manual',
+      'plugin' => 'manual',
+    ]);
+    $gateway->getPlugin()->setConfiguration([
+      'reusable' => '1',
+      'expires' => '',
+      'instructions' => [
+        'value' => 'Test instructions.',
+        'format' => 'plain_text'
+      ],
+      'payment_method_types' => ['manual'],
+    ]);
+    $gateway->save();
+
+    $profile = $this->createEntity('profile', [
+      'type' => 'customer',
+      'address' => [
+        'country_code' => 'US',
+        'postal_code' => '53177',
+        'locality' => 'Milwaukee',
+        'address_line1' => 'Pabst Blue Ribbon Dr',
+        'administrative_area' => 'WI',
+        'given_name' => 'Frederick',
+        'family_name' => 'Pabst',
+      ],
+      'uid' => $this->adminUser->id(),
+    ]);
+    $payment_method = $this->createEntity('commerce_payment_method', [
+      'uid' => $this->adminUser->id(),
+      'type' => 'manual',
+      'payment_gateway' => 'manual',
+      'billing_profile' => $profile,
+      'reusable' => TRUE,
+      'expires' => strtotime('2028/03/24'),
+    ]);
+    $payment_method->setBillingProfile($profile);
+    $payment_method->save();
+  }
+
+  /**
+   * Tests the structure of the PaymentInformation checkout pane.
+   */
+  public function testPaymentInformation() {
+    $this->drupalGet($this->product->toUrl()->toString());
+    $this->submitForm([], 'Add to cart');
+    // The order's payment method must always be available in the pane.
+    $order = Order::load(1);
+    $order->payment_method = $this->orderPaymentMethod;
+    $order->save();
+    $this->drupalGet('checkout/1');
+    $this->assertSession()->pageTextContains('Payment information');
+
+    $expected_options = [
+      'Manual for Frederick Pabst (Pabst Blue Ribbon Dr, Milwaukee)',
+      'New manual',
+    ];
+    $page = $this->getSession()->getPage();
+    foreach ($expected_options as $expected_option) {
+      $radio_button = $page->findField($expected_option);
+      $this->assertNotNull($radio_button);
+    }
+    $default_radio_button = $page->findField('Manual for Frederick Pabst (Pabst Blue Ribbon Dr, Milwaukee)');
+    $this->assertTrue($default_radio_button->getAttribute('checked'));
+  }
+
+  /**
+   * Tests checkout with an existing payment method.
+   */
+  public function testCheckoutWithExistingPaymentMethod() {
+    $this->drupalGet($this->product->toUrl()->toString());
+    $this->submitForm([], 'Add to cart');
+    $this->drupalGet('checkout/1');
+
+    $this->submitForm([
+      'payment_information[payment_method]' => '1',
+    ], 'Continue to review');
+    $this->assertSession()->pageTextContains('Payment information');
+    $this->assertSession()->pageTextContains('Manual for Frederick Pabst (Pabst Blue Ribbon Dr, Milwaukee)');
+    $this->assertSession()->pageTextContains('Expires 3/2028');
+    $this->assertSession()->pageTextContains('Frederick Pabst');
+    $this->assertSession()->pageTextContains('Pabst Blue Ribbon Dr');
+    $this->submitForm([], 'Pay and complete purchase');
+    $this->assertSession()->pageTextContains('Your order number is 1. You can view your order on your account page when logged in.');
+    $this->assertSession()->pageTextContains('Test instructions.');
+
+    $order = Order::load(1);
+    $this->assertEquals('manual', $order->get('payment_gateway')->target_id);
+    $this->assertEquals('1', $order->get('payment_method')->target_id);
+
+    // Verify that a payment was created.
+    $payment = Payment::load(1);
+    $this->assertNotNull($payment);
+    $this->assertEquals($payment->getAmount(), $order->getTotalPrice());
+    $this->assertEquals('pending', $payment->getState()->value);
+  }
+
+  /**
+   * Tests checkout with a new payment method.
+   */
+  public function testCheckoutWithNewPaymentMethod() {
+    $this->drupalGet($this->product->toUrl()->toString());
+    $this->submitForm([], 'Add to cart');
+    $this->drupalGet('checkout/1');
+    $radio_button = $this->getSession()->getPage()->findField('New manual');
+    $radio_button->click();
+    $this->waitForAjaxToFinish();
+
+    $this->submitForm([
+      'payment_information[add_payment_method][billing_information][address][0][address][given_name]' => 'Johnny',
+      'payment_information[add_payment_method][billing_information][address][0][address][family_name]' => 'Appleseed',
+      'payment_information[add_payment_method][billing_information][address][0][address][address_line1]' => '123 New York Drive',
+      'payment_information[add_payment_method][billing_information][address][0][address][locality]' => 'New York City',
+      'payment_information[add_payment_method][billing_information][address][0][address][administrative_area]' => 'NY',
+      'payment_information[add_payment_method][billing_information][address][0][address][postal_code]' => '10001',
+    ], 'Continue to review');
+    $this->assertSession()->pageTextContains('Payment information');
+    $this->assertSession()->pageTextContains('Manual for Johnny Appleseed (123 New York Drive, New York City)');
+    $this->assertSession()->pageTextContains('Expires Never');
+    $this->assertSession()->pageTextContains('Johnny Appleseed');
+    $this->assertSession()->pageTextContains('123 New York Drive');
+    $this->submitForm([], 'Pay and complete purchase');
+    $this->assertSession()->pageTextContains('Your order number is 1. You can view your order on your account page when logged in.');
+    $this->assertSession()->pageTextContains('Test instructions.');
+
+    $order = Order::load(1);
+    $this->assertEquals('manual', $order->get('payment_gateway')->target_id);
+    /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */
+    $payment_method = $order->get('payment_method')->entity;
+    $this->assertEquals('123 New York Drive', $payment_method->getBillingProfile()->get('address')->address_line1);
+
+    // Verify that a payment was created.
+    $payment = Payment::load(1);
+    $this->assertNotNull($payment);
+    $this->assertEquals($payment->getAmount(), $order->getTotalPrice());
+    $this->assertEquals('pending', $payment->getState()->value);
+  }
+
+  /**
+   * Tests that a declined payment does not complete checkout.
+   */
+  public function testCheckoutWithDeclinedPaymentMethod() {
+    $this->drupalGet($this->product->toUrl()->toString());
+    $this->submitForm([], 'Add to cart');
+    $this->drupalGet('checkout/1');
+    $radio_button = $this->getSession()->getPage()->findField('New manual');
+    $radio_button->click();
+    $this->waitForAjaxToFinish();
+
+    $this->submitForm([
+      'payment_information[add_payment_method][billing_information][address][0][address][given_name]' => 'Johnny',
+      'payment_information[add_payment_method][billing_information][address][0][address][family_name]' => 'Appleseed',
+      'payment_information[add_payment_method][billing_information][address][0][address][address_line1]' => '123 New York Drive',
+      'payment_information[add_payment_method][billing_information][address][0][address][locality]' => 'Somewhere',
+      'payment_information[add_payment_method][billing_information][address][0][address][administrative_area]' => 'WI',
+      'payment_information[add_payment_method][billing_information][address][0][address][postal_code]' => '53140',
+    ], 'Continue to review');
+    $this->assertSession()->pageTextContains('Payment information');
+    $this->assertSession()->pageTextContains('Manual for Johnny Appleseed (123 New York Drive, Somewhere)');
+    $this->assertSession()->pageTextContains('Expires Never');
+    // Change the expires time so we can get decline for the payment method.
+    $payment_method = PaymentMethod::load(2);
+    $payment_method->setExpiresTime(strtotime('-1 day'))->save();
+    $this->submitForm([], 'Pay and complete purchase');
+    $this->assertSession()->pageTextNotContains('Your order number is 1. You can view your order on your account page when logged in.');
+    $this->assertSession()->pageTextContains('We encountered an error processing your payment method. Please verify your details and try again.');
+    $this->assertSession()->addressEquals('checkout/1/order_information');
+
+    // Verify a payment was not created.
+    $payment = Payment::load(1);
+    $this->assertNull($payment);
+  }
+
+}

From 314a730e0470e4db1f2ef814f10deaa7a360e592 Mon Sep 17 00:00:00 2001
From: tavi toporjinschi <vasike@gmail.com>
Date: Fri, 17 Mar 2017 08:40:13 +0000
Subject: [PATCH 7/9] Travis warnings fix..

---
 modules/payment/tests/src/Functional/ManualPaymentAdminTest.php         | 2 +-
 modules/payment/tests/src/Functional/ManualPaymentMethodTest.php        | 2 +-
 .../tests/src/FunctionalJavascript/ManualPaymentCheckoutTest.php        | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/modules/payment/tests/src/Functional/ManualPaymentAdminTest.php b/modules/payment/tests/src/Functional/ManualPaymentAdminTest.php
index 4dadd12..027f329 100644
--- a/modules/payment/tests/src/Functional/ManualPaymentAdminTest.php
+++ b/modules/payment/tests/src/Functional/ManualPaymentAdminTest.php
@@ -91,7 +91,7 @@ protected function setUp() {
       'expires' => '',
       'instructions' => [
         'value' => 'Test instructions.',
-        'format' => 'plain_text'
+        'format' => 'plain_text',
       ],
     ]);
     $this->paymentGateway->save();
diff --git a/modules/payment/tests/src/Functional/ManualPaymentMethodTest.php b/modules/payment/tests/src/Functional/ManualPaymentMethodTest.php
index 5ffb112..a84c467 100644
--- a/modules/payment/tests/src/Functional/ManualPaymentMethodTest.php
+++ b/modules/payment/tests/src/Functional/ManualPaymentMethodTest.php
@@ -65,7 +65,7 @@ protected function setUp() {
       'expires' => '',
       'instructions' => [
         'value' => 'Test instructions.',
-        'format' => 'plain_text'
+        'format' => 'plain_text',
       ],
       'payment_method_types' => ['manual'],
     ]);
diff --git a/modules/payment/tests/src/FunctionalJavascript/ManualPaymentCheckoutTest.php b/modules/payment/tests/src/FunctionalJavascript/ManualPaymentCheckoutTest.php
index 67fbc0c..0fb15a2 100644
--- a/modules/payment/tests/src/FunctionalJavascript/ManualPaymentCheckoutTest.php
+++ b/modules/payment/tests/src/FunctionalJavascript/ManualPaymentCheckoutTest.php
@@ -94,7 +94,7 @@ protected function setUp() {
       'expires' => '',
       'instructions' => [
         'value' => 'Test instructions.',
-        'format' => 'plain_text'
+        'format' => 'plain_text',
       ],
       'payment_method_types' => ['manual'],
     ]);

From 4d5d8a4ab6679b8aafeaec002792e7e3e40911be Mon Sep 17 00:00:00 2001
From: tavi toporjinschi <vasike@gmail.com>
Date: Fri, 17 Mar 2017 11:55:37 +0000
Subject: [PATCH 8/9] Update PaymentInstructions pane code according with
 Commerce updates on chekcout panes.

---
 .../Commerce/CheckoutPane/PaymentInstructions.php  | 47 +---------------------
 1 file changed, 2 insertions(+), 45 deletions(-)

diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInstructions.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInstructions.php
index 1d0d558..984c07d 100644
--- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInstructions.php
+++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInstructions.php
@@ -2,13 +2,10 @@
 
 namespace Drupal\commerce_payment\Plugin\Commerce\CheckoutPane;
 
-use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface;
 use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneBase;
+use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneInterface;
 use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\ManualPaymentGatewayInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides the payment instructions pane.
@@ -20,47 +17,7 @@
  *   wrapper_element = "fieldset",
  * )
  */
-class PaymentInstructions extends CheckoutPaneBase implements ContainerFactoryPluginInterface {
-
-  /**
-   * The entity type manager.
-   *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
-   */
-  protected $entityTypeManager;
-
-  /**
-   * Constructs a new PaymentInstructions 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 \Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface $checkout_flow
-   *   The parent checkout flow.
-   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
-   *   The entity type manager.
-   */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow, EntityTypeManagerInterface $entity_type_manager) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition, $checkout_flow);
-
-    $this->entityTypeManager = $entity_type_manager;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow = NULL) {
-    return new static(
-      $configuration,
-      $plugin_id,
-      $plugin_definition,
-      $checkout_flow,
-      $container->get('entity_type.manager')
-    );
-  }
+class PaymentInstructions extends CheckoutPaneBase implements CheckoutPaneInterface {
 
   /**
    * {@inheritdoc}

From 57fd744a571e4b1dceb451c1007c02dc69d6a81d Mon Sep 17 00:00:00 2001
From: tavi toporjinschi <vasike@gmail.com>
Date: Fri, 17 Mar 2017 14:08:52 +0000
Subject: [PATCH 9/9] Update test for Manual payments - include Admin test for
 the Manual Gateways.

---
 .../src/Functional/ManualPaymentAdminTest.php      |   3 +-
 .../src/Functional/ManualPaymentGatewayTest.php    | 121 +++++++++++++++++++++
 2 files changed, 123 insertions(+), 1 deletion(-)
 create mode 100644 modules/payment/tests/src/Functional/ManualPaymentGatewayTest.php

diff --git a/modules/payment/tests/src/Functional/ManualPaymentAdminTest.php b/modules/payment/tests/src/Functional/ManualPaymentAdminTest.php
index 027f329..747d3b9 100644
--- a/modules/payment/tests/src/Functional/ManualPaymentAdminTest.php
+++ b/modules/payment/tests/src/Functional/ManualPaymentAdminTest.php
@@ -4,6 +4,7 @@
 
 use Drupal\commerce_payment\Entity\Payment;
 use Drupal\commerce_price\Price;
+use Drupal\Core\Url;
 use Drupal\Tests\commerce\Functional\CommerceBrowserTestBase;
 
 /**
@@ -127,7 +128,7 @@ protected function setUp() {
       'store_id' => $this->store,
     ]);
 
-    $this->paymentUri = 'admin/commerce/orders/' . $this->order->id() . '/payments';
+    $this->paymentUri = Url::fromRoute('entity.commerce_payment.collection', ['commerce_order' => $this->order->id()])->toString();
   }
 
   /**
diff --git a/modules/payment/tests/src/Functional/ManualPaymentGatewayTest.php b/modules/payment/tests/src/Functional/ManualPaymentGatewayTest.php
new file mode 100644
index 0000000..88ad489
--- /dev/null
+++ b/modules/payment/tests/src/Functional/ManualPaymentGatewayTest.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Drupal\Tests\commerce_payment\Functional;
+
+use Drupal\commerce_payment\Entity\PaymentGateway;
+use Drupal\Tests\commerce\Functional\CommerceBrowserTestBase;
+
+/**
+ * Tests the payment gateway UI for Manual type.
+ *
+ * @group commerce
+ */
+class ManualPaymentGatewayTest extends CommerceBrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'commerce_payment',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getAdministratorPermissions() {
+    return array_merge([
+      'administer commerce_payment_gateway',
+    ], parent::getAdministratorPermissions());
+  }
+
+  /**
+   * Tests creating a payment gateway.
+   */
+  public function testPaymentGatewayCreation() {
+    $this->drupalGet('admin/commerce/config/payment-gateways');
+    $this->getSession()->getPage()->clickLink('Add payment gateway');
+    $this->assertSession()->addressEquals('admin/commerce/config/payment-gateways/add');
+
+    $values = [
+      'label' => 'Example',
+      'plugin' => 'manual',
+      'configuration[mode]' => 'test',
+      'configuration[manual][reusable]' => '1',
+      'configuration[manual][expires]' => '',
+      'configuration[manual][instructions][value]' => 'Test instructions.',
+      'status' => '1',
+      'id' => 'example',
+    ];
+    $this->submitForm($values, 'Save');
+    $this->assertSession()->addressEquals('admin/commerce/config/payment-gateways');
+    $this->assertSession()->responseContains('Example');
+    $this->assertSession()->responseContains('Test');
+
+    $payment_gateway = PaymentGateway::load('example');
+    $this->assertEquals('example', $payment_gateway->id());
+    $this->assertEquals('Example', $payment_gateway->label());
+    $this->assertEquals('manual', $payment_gateway->getPluginId());
+    $this->assertEquals(TRUE, $payment_gateway->status());
+    $payment_gateway_plugin = $payment_gateway->getPlugin();
+    $this->assertEquals('test', $payment_gateway_plugin->getMode());
+    $configuration = $payment_gateway_plugin->getConfiguration();
+    $this->assertEquals('1', $configuration['reusable']);
+    $this->assertEquals('Test instructions.', $configuration['instructions']['value']);
+    $this->assertEquals('plain_text', $configuration['instructions']['format']);
+    $this->assertEmpty($configuration['expires']);
+  }
+
+  /**
+   * Tests editing a payment gateway.
+   */
+  public function testPaymentGatewayEditing() {
+    $values = [
+      'id' => 'edit_example',
+      'label' => 'Edit example',
+      'plugin' => 'manual',
+      'status' => TRUE,
+    ];
+    $payment_gateway = $this->createEntity('commerce_payment_gateway', $values);
+
+    $this->drupalGet('admin/commerce/config/payment-gateways/manage/' . $payment_gateway->id());
+    $values += [
+      'configuration[mode]' => 'live',
+      'configuration[manual][reusable]' => '1',
+      'configuration[manual][expires]' => '',
+      'configuration[manual][instructions][value]' => 'Test instructions.',
+    ];
+    $this->submitForm($values, 'Save');
+
+    \Drupal::entityTypeManager()->getStorage('commerce_payment_gateway')->resetCache();
+    $payment_gateway = PaymentGateway::load('edit_example');
+    $this->assertEquals('edit_example', $payment_gateway->id());
+    $this->assertEquals('Edit example', $payment_gateway->label());
+    $this->assertEquals('manual', $payment_gateway->getPluginId());
+    $this->assertEquals(TRUE, $payment_gateway->status());
+    $payment_gateway_plugin = $payment_gateway->getPlugin();
+    $this->assertEquals('live', $payment_gateway_plugin->getMode());
+    $configuration = $payment_gateway_plugin->getConfiguration();
+    $this->assertEquals('1', $configuration['reusable']);
+    $this->assertEquals('Test instructions.', $configuration['instructions']['value']);
+    $this->assertEquals('plain_text', $configuration['instructions']['format']);
+    $this->assertEmpty($configuration['expires']);
+  }
+
+  /**
+   * Tests deleting a payment gateway.
+   */
+  public function testPaymentGatewayDeletion() {
+    $payment_gateway = $this->createEntity('commerce_payment_gateway', [
+      'id' => 'for_deletion',
+      'label' => 'For deletion',
+      'plugin' => 'manual',
+    ]);
+    $this->drupalGet('admin/commerce/config/payment-gateways/manage/' . $payment_gateway->id() . '/delete');
+    $this->submitForm([], 'Delete');
+    $this->assertSession()->addressEquals('admin/commerce/config/payment-gateways');
+
+    $payment_gateway_exists = (bool) PaymentGateway::load('for_deletion');
+    $this->assertEmpty($payment_gateway_exists, 'The payment gateway has been deleted from the database.');
+  }
+
+}
