commerce_gc_client-8.x-1.9/src/Plugin/Commerce/PaymentGateway/GoCardlessClient.php

src/Plugin/Commerce/PaymentGateway/GoCardlessClient.php
<?php

namespace Drupal\commerce_gc_client\Plugin\Commerce\PaymentGateway;

use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\RedirectCommand;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Password\PasswordGeneratorInterface;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\Core\Url;
use Drupal\commerce_gc_client\Event\GoCardlessEvents;
use Drupal\commerce_gc_client\Event\PaymentCreatedEvent;
use Drupal\commerce_gc_client\Event\PaymentNextEvent;
use Drupal\commerce_gc_client\Event\PaymentDetailsEvent;
use Drupal\commerce_gc_client\Event\SubsDetailsEvent;
use Drupal\commerce_payment\Exception\PaymentGatewayException;
use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsitePaymentGatewayBase;
use Drupal\commerce_order\Entity\OrderInterface;
use GuzzleHttp\Exception\RequestException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides the GoCardless Client payment gateway.
 *
 * @CommercePaymentGateway(
 *   id = "gocardless_client",
 *   label = "GoCardless Client",
 *   display_label = "Pay with GoCardless",
 *   forms = {
 *     "offsite-payment" = "Drupal\commerce_gc_client\PluginForm\PaymentOffsiteForm",
 *   },
 *   modes = {
 *     "sandbox" = "Sandbox",
 *     "live" = "Live",
 *   },
 * )
 */
class GoCardlessClient extends OffsitePaymentGatewayBase {

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * The gocardless partner service.
   *
   * @var \Drupal\commerce_gc_client\GoCardlessPartner
   */
  protected $partner;

  /**
   * The request stack.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected $request;

  /**
   * The current request.
   *
   * @var \Symfony\Component\HttpFoundation\Request
   */
  protected $currentRequest;

  /**
   * The messenger service.
   *
   * @var Drupal\Core\Messenger\Messenger
   */
  protected $messenger;

  /**
   * The currency formatter service.
   *
   * @var Drupal\commerce_price\CurrencyFormatter
   */
  protected $currencyFormatter;

  /**
   * The date formatter service.
   *
   * @var \Drupal\Core\Datetime\DateFormatterInterface
   */
  protected $dateFormatter;

  /**
   * The default password generator service.
   *
   * @var \Drupal\Core\Password\DefaultPasswordGenerator
   */
  protected $passwordGenerator;

  /**
   * The httpClient service.
   *
   * @var \GuzzleHttp\Psr7\Response
   */
  protected $httpClient;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
    $instance->configFactory = $container->get('config.factory');
    $instance->partner = $container->get('commerce_gc_client.gocardless_partner');
    $instance->request = $container->get('request_stack');
    $instance->currentRequest = $container->get('request_stack')->getCurrentRequest();
    $instance->messenger = $container->get('messenger');
    $instance->currencyFormatter = $container->get('commerce_price.currency_formatter');
    $instance->dateFormatter = $container->get('date.formatter');
    $instance->passwordGenerator = $container->get('password_generator');
    $instance->httpClient = $container->get('http_client');
    return $instance;
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    return [
      'currencies' => FALSE,
      'instant_payments' => FALSE,
      'countries' => FALSE,
      'payment_limit' => 3,
      'email_warnings' => \Drupal::config('system.site')->get('mail'),
      'log_webhook' => FALSE,
      'log_api' => FALSE,
    ] + parent::defaultConfiguration();
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $form = parent::buildConfigurationForm($form, $form_state);
    $mode = $this->configuration['mode'];
    $module_settings = $this->configFactory->get('commerce_gc_client.settings');

    // Test if the payment gateway has been saved.
    $payment_gateway = $form_state->getBuildInfo()['form_id'] == 'commerce_payment_gateway_edit_form' ? TRUE : FALSE;

    // If a the payment gateway has been saved test if there is an existing
    // connection with the GoCardless partner site.
    $connected = FALSE;
    if ($payment_gateway && $module_settings->get('partner_user_' . $mode)) {
      $this->partner->setGateway($form_state->getValue('id'));
      $connected = $this->partner->api([
        'mode' => $mode,
        'method' => 'get',
        'endpoint' => 'connection',
      ]);
    }

    // If the payment gateway has not been saved disable the options to toggle
    // the mode and to 'Connect' with the GoCardless partner site.
    if (!$payment_gateway) {
      $form['mode']['#disabled'] = TRUE;
      $connect_disabled = TRUE;
    }

    $connected ? $connect_submit = 'Disconnect' : $connect_submit = 'Connect';
    $mode == 'sandbox' ? $connect_value = $connect_submit . ' SANDBOX' : $connect_value = $connect_submit . ' LIVE';

    $form['mode']['#ajax'] = ['callback' => [$this, 'modeCallback']];

    $form['connect'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('Connect with GoCardless'),
    ];
    // GoCardless verification status display, and onboarding link.
    if ($connected) {
      $result = $this->partner->api([
        'endpoint' => 'creditors',
        'action' => 'list',
      ]);
      if ($result && $result->response->status_code == 200) {
        $creditors = $result->response->body->creditors;
        if (!empty($creditors)) {
          $creditor = array_shift($creditors);
          if ($creditor->verification_status == 'successful') {
            $verified_markup = '<div class="messages messages--status">' . t('Your GoCardless account is verified.') . '</div>';
          }
          else {
            $verification_status = $creditor->verification_status == 'in_review' ? 'In review' : 'Action required';
            $client_url = Url::fromRoute('<current>', [], [
              "absolute" => TRUE,
            ])->toString();
            $partner_url = $module_settings->get('partner_url');
            $onboarding_url = $partner_url . '/gc_partner/onboarding_direct?env=' . $mode . '&client_url=' . $client_url;
            $verified_markup = '<div class="messages messages--warning">' . t("Your GoCardless account verification status is '@verification_status'. <a href='@onboarding_url'>Click here</a> to complete GoCardless onboarding and get verified.", [
              '@onboarding_url' => $onboarding_url,
              '@verification_status' => $verification_status,
            ]) . '</div>';
          }
          $form['connect']['verified'] = [
            '#markup' => $verified_markup,
          ];
        }
      }
    }

    $markup = '<br /><p><b>Connect / disconnect with GoCardless</b></p>';
    if (!$connected) {
      $markup .= "<p>After clicking 'Connect' you will be redirected to the GoCardless where you can create an account and connect your site as a client of Seamless-CMS.co.uk</p>";
      $form['connect']['markup'] = [
        '#markup' => $markup,
      ];
    }

    if (isset($connect_disabled) && $connect_disabled) {
      $connect_suffix = '<br /><i>' . $this->t('Save the form before you can connect.') . '</i>';
    }
    elseif ($mode == 'live' && !isset($_SERVER['HTTPS'])) {
      $connect_disabled = TRUE;
      $connect_suffix = '<br /><i>' . $this->t('Site needs to be secure (https) before you can connect to GoCardless LIVE.') . '</i>';
    }

    $form['connect']['submit'] = [
      '#type' => 'submit',
      '#value' => $connect_value,
      '#submit' => [[$this, 'submit' . $connect_submit]],
      '#disabled' => isset($connect_disabled) ? $connect_disabled : FALSE,
      '#suffix' => isset($connect_suffix) ? $connect_suffix : NULL,
      '#attributes' => $connected ? ['onclick' => 'if (!confirm("Are you sure you want to disconnect your site from GoCardless?")) {return false;}'] : NULL,
    ];

    if ($connected) {
      if (!$webhook_secret = $module_settings->get('webhook_secret_' . $mode)) {
        $webhook_secret = $this->partner->api([
          'endpoint' => 'webhook_secret',
        ])->response;
        $settings = $this->configFactory->getEditable('commerce_gc_client.settings');
        $settings->set('webhook_secret_' . $mode, $webhook_secret)->save();
      }
      // This should be obtained via container injection.
      $base_url = $this->currentRequest->getSchemeAndHttpHost();
      $webhook_url = $base_url . '/gc_client/webhook';
      $gc_webhook_url = $this->t('https://manage@env.gocardless.com/developers/webhook-endpoints', [
        '@env' => $mode == 'sandbox' ? '-sandbox' : '',
      ]);
      $webhook_secret_markup = "<p id='webhook_secret'><b>" . $this->t('Webhook secret:') . "</b> " . $webhook_secret . "</p>";
      $webhook_secret_markup .= '<p>' . $this->t('To receive webhooks create / update a Webhook Endpoint at your GoCardless account <a target="new" href="@gc_webhook_url">here</a>, and set the Webhook URL as <i>@webhook_url</i>, and the Webhook Secret as the random 30 byte string that has been generated for you above. The Client Certificate is optional, and your webhooks will work perfectly well without one.', [
        '@webhook_url' => $webhook_url,
        '@gc_webhook_url' => $gc_webhook_url,
      ]) . '</p>';
      $form['connect']['webhook_secret_markup'] = [
        '#title' => $this->t('Webhook secret'),
        '#type' => 'markup',
        '#markup' => $webhook_secret_markup,
      ];
      $form['connect']['webhook_submit'] = [
        '#type' => 'submit',
        '#value' => $this->t('Change secret'),
        '#suffix' => $this->t('If you change this, and have already set up your webhook endpoint in your GoCardless account, you will need to update it there as well.'),
        '#ajax' => [
          'callback' => [$this, 'webhookSecretCallback'],
          'wrapper' => 'webhook_secret',
        ],
      ];
    }

    // Global.
    $form['global'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('General settings'),
    ];

    $fx_payments = TRUE;
    if (!isset($creditor) || (
        isset($creditor) && !$creditor->fx_payout_currency)) { 
      $fx_payments = FALSE;
      $form['global']['currency_markup'] = [
        '#type' => 'markup',
        '#markup' => $this->t("The following two settings use GoCardless international payment features. If the checkboxes are disabled it is because you need to enable FX Payments on your GoCardless account before you can use them. To do this contact <a href='mailto:help@gocardless.com'>help@gocardless.com</a>."),
      ];
    }
    $form['global']['currencies'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Customer can choose currency at GoCardless'),
      '#default_value' => $this->configuration['currencies'],
      '#description' => $this->t("Only applies to Redirect Flows:- checkouts that do not result in an Instant Payment creation. Without this setting, customer's are restricted to creating debit mandates in the currency of their order. When this option is enabled the customer is able to choose from all the currencies that you have available at GoCardless. The amount of any payments that are created through the mandate are automatically adjusted using the latest, real exchange rates provided by GoCardless. It is not recommended that you use Subscription payment types if you have this option selected (or if you use the Commerce Currency Resolver module) - see the README file for more details. Go to <a href='/admin/commerce/config/currencies/gocardless'>here</a> for more options, and information on enabling multiple currencies with GoCardless."),
      '#disabled' => $fx_payments ? FALSE : TRUE,
    ];

    $form['global']['instant_payments'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Use Instant Payments if available'),
      '#default_value' => $this->configuration['instant_payments'],
      '#description' => $this->t("Instant Payments are single payments created upon checkout by GoCardless using Open Banking protocol. The range of countries that GoCardless provides Instant Payments for is currently limited to the UK and Germany. If this option is not selected all checkouts will result in the creation of a debit mandate. If you are using Instant Payments, and one or more of the order items uses GoCardless recurring rules, then the first payment for these items will be included in the Instant Payment, and subsequent payments will be created under a debit mandate. The Instant Payment checkout experience can be confusing for end-users so it is recommended you try it out and decide if you think it is suitable to use."),
      '#disabled' => $fx_payments ? FALSE : TRUE,
    ];

    $form['global']['countries'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Automatically select country for Instant Payments'),
      '#default_value' => $this->configuration['countries'],
      '#description' => $this->t("Only applies to checkouts that request the creation of an Instant Payment. With this enabled, the currency of the payment is determined by the customer's billing address country. If the currency of the country is different to the currency of the order then the payment amount will be adjusted using the latest real exchange rates provided by GoCardless. If the customer's country is not provided for then the checkout will fallback to using a debit mandate instead of an Instant Payment."),
      '#disabled' => $fx_payments ? FALSE : TRUE,
      '#states' => [
        'visible' => [
          ':input[name="configuration[gocardless_client][global][instant_payments]"]' => ['checked' => TRUE],
        ],
      ],
    ];

    $form['global']['payment_limit'] = [
      '#type' => 'number',
      '#title' => $this->t('Maximum payments'),
      '#default_value' => $this->configuration['payment_limit'],
      '#size' => 3,
      '#min' => 1,
      '#step' => 1,
      '#description' => $this->t("The maximum number of payments that can be raised automatically, per order, per day. If the amount is exceeded, a warning email is sent to the specified address below. Leave unset for unlimitted."),
      '#required' => FALSE,
    ];

    $form['global']['email_warnings'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Email'),
      '#default_value' => $this->configuration['email_warnings'],
      '#description' => $this->t("Email address to send warnings."),
      '#size' => 40,
      '#maxlength' => 40,
    ];

    // Logging options.
    $form['log'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('Logging'),
    ];

    $form['log']['log_webhook'] = [
      '#prefix' => '<p>',
      '#suffix' => '</p>',
      '#type' => 'checkbox',
      '#title' => '<b>' . $this->t('Enable webhook logging') . '</b>',
      '#description' => $this->t('Webhooks recieved from GoCardless will be written to the log.'),
      '#default_value' => $this->configuration['log_webhook'],
    ];

    $form['log']['log_api'] = [
      '#prefix' => '<p>',
      '#suffix' => '</p>',
      '#type' => 'checkbox',
      '#title' => '<b>' . $this->t('Enable API logging') . '</b>',
      '#description' => $this->t('Responses from the Partner site to API posts will be written to the log.'),
      '#default_value' => $this->configuration['log_api'],
    ];
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
    parent::submitConfigurationForm($form, $form_state);
    if (!$form_state->getErrors()) {
      $values = $form_state->getValue($form['#parents']);
      $this->configuration['currencies'] = $values['global']['currencies'];
      $this->configuration['instant_payments'] = $values['global']['instant_payments'];
      $this->configuration['countries'] = $values['global']['countries'];
      $this->configuration['email_warnings'] = $values['global']['email_warnings'];
      $this->configuration['payment_limit'] = $values['global']['payment_limit'];
      $this->configuration['log_api'] = $values['log']['log_api'];
      $this->configuration['log_webhook'] = $values['log']['log_webhook'];
    }
  }

  /**
   * {@inheritdoc}
   */
  public function onReturn(OrderInterface $order, Request $request) {
    $billing_request_id = $request->query->get('billing_request_id');
    $payment_gateway = $order->get('payment_gateway')->first()->entity;
    $this->partner->setGateway($payment_gateway->Id());
    $result = $this->partner->api([
      'endpoint' => 'billing_requests',
      'action' => 'get',
      'id' => $billing_request_id,
    ]);

    if ($result && $result->response->status_code == 200) {
      $billing_request = $result->response->body->billing_requests;
    }
    else {
      throw new PaymentGatewayException('Billing Request creation was unsuccessful with order.');
    }

    if (isset($billing_request->payment_request)) {
      $payment = $billing_request->payment_request;
      $payment_currency = $payment->currency;
      $payment_amount = $payment->amount / 100;
      $message_amount = $this->currencyFormatter->format($payment_amount, $payment_currency);

      if (!$billing_request->fallback_occurred) {
        $message = $this->t('You have made an Instant payment of @amount via GoCardless for your order #@order_id.', array(
          '@order_id' => $order->id(),
          '@amount' => $message_amount, 
        ));
      }
      else {
        $message = $this->t('A payment of @amount has been created via GoCardless with your debit mandate and will be paid from your bank account in 3-4 working days.', array(
          '@amount' => $message_amount, 
        ));
      }
      $this->messenger->addMessage($message);
    }

    if (isset($billing_request->mandate_request)) {
      $payment_currency = $billing_request->mandate_request->currency;
      if (isset($billing_request->links->mandate_request_mandate)) {
        $mandate_id = $billing_request->links->mandate_request_mandate;
        $message = $this->t('Your new debit mandate @mandate has been created by GoCardless.', [
          '@mandate' => $mandate_id,
        ]);
        $this->messenger->addMessage($message);
      }
      elseif (isset($billing_request->links->mandate_request)) {
        $message = $this->t('A new debit mandate is being created for you by GoCardless.');
        $this->messenger->addMessage($message);
      }
    }

    $order_currency = $order->getTotalPrice()->getCurrencyCode();
    if ($order_currency !== $payment_currency) {
      $currency_message = $this->t('The price of your order has been converted from @order_currency to @payment_currency using the latest, real currency exchange rates provided by GoCardless.', array(
        '@order_currency' => $order_currency,
        '@payment_currency' => $payment_currency,
      ));
      $this->messenger->addMessage($currency_message);
    }

    // If the mandate has been created during the checkout process, process
    // order mandate now. In some cases GoCardless does not complete the
    // mandate creation until after the customer has been returned to the
    // site, in which case the mandate processing function is fired by a
    // webhook instead. This occurs very quickly after the order is completed.
    if (isset($mandate_id)) {
      $gcid = self::processMandate($order, $billing_request, $mandate_id); 
    }

    // Process any One-off payments or subscriptions if required.
    foreach ($order->getItems() as $item) {

      if ($data = $item->getData('gc')) {
        $item_id = $item->id();
        
        // One-off payments
        if ($data['gc_type'] == 'P') {
          $calculate = commerce_gc_client_price_calculate($order, $item);

          if (array_key_exists('gc_end_type', $data)) {
            if ($data['gc_end_type'] == 'fixed') {
              $data['gc_end_time'] = strtotime('midnight +1 day', strtotime($data['gc_end_date']));
            }
            elseif ( $data['gc_end_type'] == 'relative') {
              $deferral = '+' . $data['gc_end_number'] . $data['gc_end_unit'];
              $time = strtotime($deferral, strtotime('midnight'));
              $data['gc_end_date'] = $this->dateFormatter->format($time, 'gocardless');
              $data['gc_end_time'] = strtotime('+1 day', $time);
            }
            $item->setData('gc', $data)->save();
          }

          if (isset($mandate_id)) {
            $payment_request = isset($billing_request->links->payment_request) ? true : false;
            self::processPayment($payment_request, $this->partner, $item, $data, $calculate, $mandate_id, $gcid);
          }
          
          if (isset($data['interval_params']) && $data['gc_create_payment']) {
            $next_payment = strtotime('+' . $data['interval_params']['string']);
            $amount = $this->currencyFormatter
              ->format($calculate['amount'], $calculate['currency_code']);
            $next_payment_date = $this->dateFormatter->format($next_payment, 'gocardless_client');

            $message = t("The next recurring payment of @amount for @product_title has been scheduled and will take place soon after @next_payment_date.", array(
              '@amount' => $amount,
              '@product_title' => $item->getTitle(),
              '@next_payment_date' => $next_payment_date,
            ));
            $this->messenger->addMessage($message);
          }
        }

        // Subscriptions
        elseif ($data['gc_type'] == 'S') {
          $calculate = commerce_gc_client_price_calculate($order, $item);
          if (isset($mandate_id)) {
            self::processSubscription($this->partner, $item, $data, $calculate, $mandate_id, $gcid);
          }
          else {
            $interval = $data['interval_params']['length'];
            $amount = $this->currencyFormatter->format($calculate['amount'], $calculate['currency_code']);
            $message_params = [
              '@product' => $item->getTitle(),
              '@interval' => $interval == 1 ? '' : $interval . ' ',
              '@interval_unit' => $data['interval_params']['unit'],
              '@amount' => $amount,
            ];
            $message = $this->t('Your @interval@interval_unit subscription of @amount for <b>@product</b> will be created by GoCardless after the mandate creation has completed.', $message_params);
            $this->messenger->addMessage($message);
          }
        }
      }
    }
  }

  /**
   * Add the order mandate details to the database.
   *
   * @param object $order
   *   The Drupal Commerce order entity.
   * @param object $billing_request
   *   The GoCardless billing request object.
   * @param string $mandate
   *   The Id of the GoCardless mandate object.
   *
   * @return int
   *   The Id of the inserted database entry.
   */
  public static function processMandate($order, $billing_request, $mandate_id) {
    $payment_gateway = $order->get('payment_gateway')->first()->entity;
    $mode = $payment_gateway->get('configuration')['mode'];
    $created = \Drupal::time()->getRequestTime();
    $db = \Drupal::service('database');
    $gcid = $db->insert('commerce_gc_client')->fields([
      'order_id' => $order->id(),
      'gc_mandate_id' => $mandate_id,
      'gc_mandate_scheme' => $billing_request->mandate_request->scheme,
      'gc_mandate_status' => 'pending_submission',
      'created' => $created,
      'data' => serialize([
        'gc_billing_request_id' => $billing_request->id,
        'gc_customer_id' => $billing_request->links->customer,
      ]),
      'sandbox' => $mode == 'sandbox' ? 1 : 0,
    ])->execute();
    return $gcid;
  }

  /**
   * Processes one-off payments when mandate is available.
   *
   * @param bool $payment_request
   *   True if the payment was created by GoCardless as an Instant payment.
   * @param object $partner
   *   The Commerce GoCardless Client Partner service.
   * @param object $item
   *   The Commerce Order Item entity.
   * @param array $data
   *   Module relevant data for the Commerce Order Item entity.
   * @param object $billing_request
   *   The GoCardless billing request object.
   * @param array $calculate
   *   Data relating to the GoCardless payment being processed.
   * @param string $mandate_id
   *   The Id of the GoCardless mandate object.
   * @param int $gcid
   *   An Id of data in the commerce_gc_client table.
   * @param bool $on_return
   *   True if the function is called in checkout.
   */
  public static function processPayment($payment_request, $partner, $item, $data, $calculate, $mandate_id, $gcid, $on_return = true) {
    $item_id = $item->id();
    $db = \Drupal::service('database');
    $messenger = \Drupal::messenger();
    $uuid_service = \Drupal::service('uuid');
    $date_formatter = \Drupal::service('date.formatter');
    $event_dispatcher = \Drupal::service('event_dispatcher');
    $currency_formatter = \Drupal::service('commerce_price.currency_formatter');

    $next_payment = NULL;
    $first_payment = count($data) == 1 ? 1 : self::startDateCalculate($data);

    // Dispatch an event so that the first_payment date can be altered by
    // other modules.
    $first_payment_event = new PaymentNextEvent($first_payment, $item_id, 'first_payment');
    $event_dispatcher->dispatch($first_payment_event, GoCardlessEvents::PAYMENT_NEXT);
    $first_payment = $first_payment_event->getNextPayment();

    // If the product is set to create the first payment immediately
    if ($first_payment === true) {
      // Then create a payment only if an instant payment wasn't created during
      // the billing request flow.
      if (!$payment_request) {
        $payment_details = [
          'endpoint' => 'payments',
          'action' => 'create',
          'mandate' => $mandate_id,
          'amount' => $calculate['amount'],
          'currency' => $calculate['currency_code'],
          'description' => t('Payment for') . ' ' . $item->getTitle(),
          'metadata' => [
            'item_id' => $item_id,
          ],
          'idempotency_key' => $uuid_service->generate(),
        ];

        // Dispatch an event so that the payment details array can be altered
        // by other modules before sending to GoCardless.
        $payment_details_event = new PaymentDetailsEvent($payment_details, $item_id, 'checkout');
        $event_dispatcher->dispatch($payment_details_event, GoCardlessEvents::PAYMENT_DETAILS);
        $payment_details = $payment_details_event->getPaymentDetails();

        // Create a payment.
        $result = $partner->api($payment_details);
        if ($result->response->status_code == 201) {
          $payment = $result->response->body->payments;
          if (isset($data['interval_params'])) {
            $next_payment = strtotime('+' . $data['interval_params']['string']);
          }
          if ($on_return) {
            $amount = $currency_formatter->format($payment->amount / 100, $calculate['currency_code']);
            $charge_date = $date_formatter->format(strtotime($payment->charge_date), 'gocardless_client');
            $message_params = [
              '@product' => $item->getTitle(),
              '@amount' => $amount,
              '@charge_date' => $charge_date,
            ];
            $message = t('A payment of @amount for <b>@product</b> has been created and will be paid from your bank on @charge_date.', $message_params);
            $messenger->addMessage($message);
          }
        }
        else {
          if (!isset($result->response) || !$result->response || $result->response->status_code == 500) {
            $next_payment = $created;
            $data['payment_details'] = $payment_details;
            $item->setData('gc', $data)->save();
            if ($on_return) {
              $messenger->addWarning(t('There was a problem creating your initial payment at GoCardless, so we will resubmit it later.'));
            }
          }
          elseif ($on_return) {
            $messenger->addWarning(t('Something went wrong creating your initial payment with GoCardless. Please contact the site administrator for assistance.'));
          }
        }
      }

      //Update next payment date for items included in payment request. 
      elseif (isset($data['interval_params'])) {
        $next_payment = strtotime('+' . $data['interval_params']['string']);
      }
    }

    // Else if a first_payment date is set for the product then defer the
    // first payment creation.
    elseif ($first_payment) {
      $next_payment = $first_payment;
      if ($on_return) {
        $initial_payment = $currency_formatter->format($calculate['amount'], $calculate['currency_code']);
        $charge_date = $date_formatter->format(strtotime('midnight', $first_payment), 'gocardless_client');
        $message = t('A payment for @initial_payment will be created with GoCardless on @charge_date.', [
          '@initial_payment' => $initial_payment,
          '@charge_date' => $charge_date,
        ]);
        $messenger->addMessage($message);
      }
    }

    // Dispatch an event so that the next_payment date can be altered by
    // other modules. Todo could cause a problem if there is no response
    // or a 500 response on the payment creation, as the payment will not
    // be submitted with the same idempotency key.
    $next_payment_event = new PaymentNextEvent($next_payment, $item_id, 'checkout');
    $event_dispatcher->dispatch($next_payment_event, GoCardlessEvents::PAYMENT_NEXT);
    $next_payment = $next_payment_event->getNextPayment();

    $db->insert('commerce_gc_client_item')
      ->fields([
        'item_id' => $item_id,
        'gcid' => $gcid,
        'type' => 'P',
        'next_payment' => $next_payment,
      ])
      ->execute();

    if (isset($payment)) {
      // Dispatch an event so that other modules can respond to payment
      // creation.
      $payment_created_event = new PaymentCreatedEvent($payment, $item_id, 'checkout');
      $event_dispatcher->dispatch($payment_created_event, GoCardlessEvents::PAYMENT_CREATED);
    }
  }

  /**
   * Processes subscriptions when mandate is available.
   *
   * @param object $partner
   *   The Commerce GoCardless Client Partner service.
   * @param object $item
   *   The Commerce Order Item entity.
   * @param array $data
   *   Module relevant data for the Commerce Order Item entity.
   * @param array $calculate
   *   Data relating to the GoCardless payment being processed.
   * @param string $mandate_id
   *   The Id of the GoCardless mandate object.
   * @param int $gcid
   *   An Id of data in the commerce_gc_client table.
   * @param bool $on_return
   *   True if the function is called in checkout.
   */
  public static function processSubscription($partner, $item, $data, $calculate, $mandate_id, $gcid, $on_return = true) {
    $item_id = $item->id();
    $db = \Drupal::service('database');
    $messenger = \Drupal::messenger();
    $uuid_service = \Drupal::service('uuid');
    $date_formatter = \Drupal::service('date.formatter');
    $event_dispatcher = \Drupal::service('event_dispatcher');
    $currency_formatter = \Drupal::service('commerce_price.currency_formatter');

    $start_date = NULL;
    $end_date = NULL;
    if (isset($data['gc_start_type'])) {
      if ($data['gc_start_type'] == 'fixed' || ($data['gc_start_type'] == 'time' && $data['gc_start_date'])) {
        $start_date = $date_formatter->format(strtotime($data['gc_start_date']), 'gocardless');
      }
      elseif ($data['gc_start_type'] == 'relative') {
        $deferral = '+' . $data['gc_start_number'] . $data['gc_start_unit'];
        $time = strtotime($deferral, strtotime('midnight'));
        $start_date = $date_formatter->format($time, 'gocardless');
      }  
      if ($data['gc_end_type'] == 'fixed') {
        $end_date = $date_formatter->format(strtotime($data['gc_end_date']), 'gocardless');
      }
      elseif ($data['gc_end_type'] == 'relative') {
        $deferral = '+' . $data['gc_end_number'] . $data['gc_end_unit'];
        $time = strtotime($deferral, strtotime('midnight'));
        $end_date = $date_formatter->format($time, 'gocardless');
      }
      $count = $data['gc_end_type'] == 'count' ? $data['gc_count'] : NULL;
      $dom = $data['gc_start_type'] == 'time' && $data['gc_interval_unit'] !== 'weekly' ? $data['gc_dom'] : NULL; 
      $month = $data['gc_start_type'] == 'time' && $data['gc_interval_unit'] == 'yearly' ? $data['gc_month'] : NULL; 
    }  
    
    // For product variations created pre module version 2.1.1
    else {
      if ($data['gc_start_date']) {
        $start_date = $date_formatter->format(strtotime($data['gc_start_date']), 'gocardless');
      }
      if ($data['gc_end_date']) {
        $end_date = $date_formatter->format(strtotime($data['gc_end_date']), 'gocardless');
      }
      $count = $data['gc_count'] ? $data['gc_count'] : NULL;
      $dom = $data['gc_dom'] ? $data['gc_dom'] : NULL;
      $month = $data['gc_month'] ? $data['gc_month'] : NULL;
    }

    $interval = $data['interval_params']['length'];
    $subs_details = [
      'endpoint' => 'subscriptions',
      'action' => 'create',
      'mandate' => $mandate_id,
      'amount' => $calculate['amount'],
      'currency' => $calculate['currency_code'],
      'name' => t('Subscription for ') . $item->getTitle(),
      'interval' => $interval,
      'interval_unit' => $data['interval_params']['unit'],
      'start_date' => $start_date,
      'count' => $count,
      'end_date' => $end_date,
      'day_of_month' => $dom,
      'month' => $month,  
      'metadata' => [
        'item_id' => $item_id,
      ],
      'idempotency_key' => $uuid_service->generate(),
    ];

    // Dispatch an event so that the subscription details array can be
    // altered by other modules before sending to GoCardless.
    $subs_details_event = new SubsDetailsEvent($subs_details, $item_id);
    $event_dispatcher->dispatch($subs_details_event, GoCardlessEvents::SUBS_DETAILS);
    $subs_details = $subs_details_event->getSubsDetails();
    $result = $partner->api($subs_details);

    if ($result->response->status_code == 201) {
      $sub = $result->response->body->subscriptions;

      $db->insert('commerce_gc_client_item')
        ->fields([
          'gcid' => $gcid,
          'item_id' => $item_id,
          'type' => 'S',
          'gc_subscription_id' => $sub->id,
        ])
        ->execute();

      if ($on_return) {
        $amount = $currency_formatter->format($sub->amount / 100, $calculate['currency_code']);
        $first_payment = strtotime(array_shift($sub->upcoming_payments)->charge_date);
        $first_payment_date = $date_formatter->format($first_payment, 'gocardless_client');

        $message_params = [
          '@product' => $item->getTitle(),
          '@interval' => $interval == 1 ? '' : $interval . ' ',
          '@interval_unit' => $sub->interval_unit,
          '@amount' => $amount,
          '@first_payment' => $first_payment_date,
        ];
        $message = t('Your @interval@interval_unit subscription of @amount for <b>@product</b> has been created with GoCardless, and the first payment will be charged from your bank on @first_payment.', $message_params);
        $messenger->addMessage($message);
      }
    }
    else {
      if (!isset($result->response) || !$result->response || $result->response->status_code == 500) {
        if ($on_return) {
          $messenger->addWarning(t('There was a problem creating your subscription at GoCardless, so we will resubmit it later.'));
        }
        $db->insert('commerce_gc_client_item')->fields([
          'gcid' => $gcid,
          'item_id' => $item_id,
          'type' => $data['gc_type'],
          'next_payment' => \Drupal::time()->getRequestTime(),
        ])->execute();

        $data['subs_details'] = $subs_details;
        $item->setData('gc', $data)->save();
      }
      elseif($on_return) {
        $messenger->addError(t('Something went wrong creating your subscription with GoCardless. Please contact the site administrator for assistance.'));
      }
    }
  }

  /**
   * Calculates first payment creation date for an order item.
   *
   * Returns TRUE if the product variation is set to create a payment
   * immediately, or a Unix timestamp if a first payment date has been
   * calculated.
   *
   * @param array $data
   *   The data array for the new order item.
   * @param string $date
   *   (optional) A date as a string. For testing purposes only.
   *
   * @return mixed
   *   Bool or integer.
   */
  public static function startDateCalculate(array $data, $date = FALSE) {
    if ($data['gc_create_payment']) {
      return true;
    }
    $time = $date ? strtotime($date) : strtotime('midnight');
    
    if (array_key_exists('gc_start_type', $data)) {
      
      if ($data['gc_start_type'] == 'relative') {
        $deferral = '+' . $data['gc_start_number'] . $data['gc_start_unit'];
        return strtotime($deferral, $time);
      }
      
      elseif ($data['gc_start_type'] == 'fixed') {
        return strtotime($data['gc_start_date']); 
      }
      
      elseif ($data['gc_start_type'] == 'time') {
        $start_time = $data['gc_start_date'] ? strtotime($data['gc_start_date']) : $time;

        // Weekly intervals.
        if ($data['gc_interval_unit'] == 'weekly') {
          $dow = date('l', $start_time);
          if ($dow == $data['gc_dow'] || !$data['gc_dow']) {
            $start_time > $time ? $fp = $start_time : $fp = $time;
          }
          else {
            $start_time < $time ? $start_time = $time : NULL;
            $fp = strtotime('next ' . $data['gc_dow'], $start_time);
          }
          return ($fp);
        }
  
        // Monthly intervals.
        elseif ($data['gc_interval_unit'] == 'monthly') {
          $data['gc_dom'] == -1 ? $string = 'last day of ' : $string = $data['gc_dom'];
          $start_time < $time ? $start_time = $time : NULL;
          $m = date('M', $start_time);
          $y = date('Y', $start_time);
          $fp = strtotime($string . $m . $y);
          if ($fp < $start_time) {
            $fp = strtotime('+1 month', $fp);
          }
          return $fp;
        }
  
        // Yearly intervals.
        elseif ($data['gc_interval_unit'] == 'yearly') {
          $data['gc_dom'] == -1 ? $string = 'last day of ' : $string = $data['gc_dom'];
          $y = date('Y', $start_time);
          $fp = strtotime($string . $data['gc_month'] . $y);
          if ($fp < $start_time) {
            $fp = strtotime('+1 year', $fp);
            // Take account of leap years.
            if (date('n', $fp) == 2 && $data['gc_dom'] == -1) {
              $fp = strtotime('last day of ' . date('M', $fp), $fp);
            }
          }
          return $fp;
        }
      }
      
      else {
        return false;
      }
    }
    
    // Legacy code for product variations created pre module version 2.1.1
    else {
      $start_time = $data['gc_start_date'] ? strtotime($data['gc_start_date']) : $time;

      // Weekly intervals.
      if ($data['gc_interval_unit'] == 'weekly') {
        $dow = date('l', $start_time);
        if ($dow == $data['gc_dow'] || !$data['gc_dow']) {
          $start_time > $time ? $fp = $start_time : $fp = $time;
        }
        else {
          $start_time < $time ? $start_time = $time : NULL;
          $fp = strtotime('next ' . $data['gc_dow'], $start_time);
        }
        return ($fp);
      }

      // Monthly intervals.
      elseif ($data['gc_interval_unit'] == 'monthly') {
        $data['gc_dom'] == -1 ? $string = 'last day of ' : $string = $data['gc_dom'];
        $start_time < $time ? $start_time = $time : NULL;
        $m = date('M', $start_time);
        $y = date('Y', $start_time);
        $fp = strtotime($string . $m . $y);
        if ($fp < $start_time) {
          $fp = strtotime('+1 month', $fp);
        }
        return $fp;
      }

      // Yearly intervals.
      elseif ($data['gc_interval_unit'] == 'yearly') {
        $data['gc_dom'] == -1 ? $string = 'last day of ' : $string = $data['gc_dom'];
        $y = date('Y', $start_time);
        $fp = strtotime($string . $data['gc_month'] . $y);
        if ($fp < $start_time) {
          $fp = strtotime('+1 year', $fp);
          // Take account of leap years.
          if (date('n', $fp) == 2 && $data['gc_dom'] == -1) {
            $fp = strtotime('last day of ' . date('M', $fp), $fp);
          }
        }
        return $fp;
      }

      // No interval set.
      elseif ($data['gc_start_date']) {
        $fp = strtotime($data['gc_start_date']);
        if ($fp > $time) {
          return $fp;
        }
      }
      return $time;
    }
  }

  /**
   * Form submission handler for buildConfigurationForm().
   *
   * Initiate the GoCardless OAuth Flow to connect the site as a client
   * of the Seamless-CMS GoCardless partner app.
   */
  public function submitConnect(array &$form, FormStateInterface $form_state) {
    $settings = $this->configFactory->getEditable('commerce_gc_client.settings');
    $env = $form_state->getValue('configuration')['gocardless_client']['mode'];
    $partner_url = $settings->get('partner_url');
    $url = $partner_url . '/user/register?_format=json';
    $name = $this->passwordGenerator->generate(24);
    $pass = $this->passwordGenerator->generate(24);
    $body = [
      'name' => ['value' => $name],
      'mail' => ['value' => $name . '@seamless-cms.co.uk'],
      'pass' => ['value' => $pass],
    ];

    try {
      $result = $this->httpClient->post($url, [
        'headers' => ['Content-Type' => 'application/json'],
        'body' => json_encode($body),
      ]);
      if ($result->getStatusCode() == 200) {
        $settings->set('partner_user_' . $env, $name);
        $settings->set('partner_pass_' . $env, $pass);
        $settings->save();

        $gateway_id = $form_state->getvalue('id');
        $base_url = $this->currentRequest->getSchemeAndHttpHost();
        $client_url = urlencode($base_url . '/gc_client/connect_complete');
        $site_email = $this->configFactory->get('system.site')->get('mail');
        $connect_url = Url::fromUri($partner_url . '/gc_client/connect', [
          'query' => [
            'env' => $env,
            'name' => $name,
            'mail' => $site_email,
            'client_url' => $client_url,
            'gateway_id' => $gateway_id,
            'module' => 'Commerce 2.1.x',
          ], 
        ]);
        $response = new TrustedRedirectResponse($connect_url->toString());
        $form_state->setresponse($response);
        $this->currentRequest->query->remove('destination');
      }
      else {
        $this->messenger->addError($this->t('There was a problem connecting Seamless-CMS with GoCardless, please try again later.'));
        return;
      }
    }
    catch (RequestException $e) {
      $this->messenger->addError($e->getMessage());
      return;
    }
  }

  /**
   * Form submission handler for buildConfigurationForm().
   *
   * Disconnects client site from GC partner site.
   */
  public function submitDisconnect(array &$form, FormStateInterface $form_state) {
    $this->partner->setGateway($form_state->getValue('id'));
    $result = $this->partner->api([
      'endpoint' => 'oauth',
      'action' => 'revoke',
    ]);

    if ($result && $result->response == 200) {
      // TODO The following should only happen if there are no other GoC
      // payment gateways installed.
      $session = $this->request->getSession();
      if ($session->get('commerce_gc_client_cookie_created')) {
        $session->remove('commerce_gc_client_cookie_created');
      }
      $mode = $form_state->getValue('configuration')['gocardless_client']['mode'];
      $config = $this->configFactory->getEditable('commerce_gc_client.settings');
      $config->set('partner_user_' . $mode, NULL)->save();
      $config->set('partner_pass_' . $mode, NULL)->save();
      $this->messenger->addMessage($this->t('You have disconnected successfully from GoCardless'));
    }
    else {
      $this->messenger->addError($this->t('There was a problem disconnecting from GoCardless'));
    }

    // Remove the 'desination=' parameter from query string if it exists.
    $this->currentRequest->query->remove('destination');
  }

  /**
   * Ajax callback function.
   *
   * On changing 'mode', saves new configuration and reloads the page.
   *
   * @see buildConfigurationForm()
   */
  public function modeCallback(array &$form, FormStateInterface $form_state) {
    $gateway_id = $form_state->getValue('id');
    $mode = $form_state->getValue('configuration')['gocardless_client']['mode'];
    $this->configFactory->getEditable('commerce_payment.commerce_payment_gateway.' . $gateway_id)->set('configuration.mode', $mode)->save();

    if (empty($gateway_id)) {
      $path = '/admin/commerce/config/payment-gateways/add';
    }
    else {
      $path = '/admin/commerce/config/payment-gateways/manage/' . $gateway_id;
    }
    $response = new AjaxResponse();
    $response->addCommand(new RedirectCommand($path));
    return $response;
  }

  /**
   * Ajax callback function.
   *
   * On submitting 'Change secret' a new client secret is obtained, saved to
   * the module config and returned for display on the settings form.
   *
   * @return array
   *   The new render array for display on the settings form.
   *
   * @see buildConfigurationForm()
   */
  public function webhookSecretCallback(array &$form, FormStateInterface $form_state) {
    $this->messenger->addWarning(t('You have changed your webhook secret. If you have already added a secret to your GoCardless account, you will have to update it there as well.'));
    $mode = $this->configuration['mode'];
    $webhook_secret = $this->partner->api([
      'endpoint' => 'webhook_secret',
      'method' => 'get',
    ]);
    $settings = $this->configFactory->getEditable('commerce_gc_client.settings');
    $settings->set('webhook_secret_' . $mode, $webhook_secret)->save();
    $output = "<p id='webhook_secret'><b>" . $this->t('Webhook secret:') . "</b> " . $webhook_secret . "</p>";
    return ['#markup' => $output];
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc