commerce_gc_client-8.x-1.9/src/Controller/WebhookHandler.php

src/Controller/WebhookHandler.php
<?php

namespace Drupal\commerce_gc_client\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher;
use Drupal\commerce_order\Entity\Order;
use Drupal\commerce_price\CurrencyFormatter;
use Drupal\commerce_price\Price;
use Drupal\commerce_payment\Entity\Payment;
use Drupal\commerce_gc_client\GoCardlessPartner;
use Drupal\commerce_gc_client\Event\GoCardlessEvents;
use Drupal\commerce_gc_client\Event\WebhookEvent;
use Drupal\commerce_gc_client\Plugin\Commerce\PaymentGateway\GoCardlessClient;
use Drupal\mysql\Driver\Database\mysql\Connection;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;

/**
 * Controller for handling webhooks from GoCardless.com.
 */
class WebhookHandler extends ControllerBase {

  /**
   * The database driver connection.
   *
   * @var \Drupal\mysql\Driver\Database\mysql\Connection
   */
  protected $db;

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

  /**
   * The event dispatcher.
   *
   * @var \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher
   */
  protected $eventDispatcher;

  /**
   * The "commerce_log" entity type storage.
   *
   * @var \Drupal\Core\Entity\EntityTypeManager
   */
  private $logStorage;

  /**
   * The "commerce_payment" entity type storage.
   *
   * @var \Drupal\Core\Entity\EntityTypeManager
   */
  private $paymentStorage;

  /**
   * The logger.
   *
   * @var \Psr\Log\LoggerInterface
   */
  private $logger;

  /**
   * The configuration settings for the GoCardless Client payment gateway.
   *
   * @var array
   */
  private $paymentGatewaySettings;

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

  /**
   * The GoCardless Partner service.
   *
   * @var \Drupal\commerce_gc_client\GoCardlessPartner
   */
  private $partner;

  /**
   * Constructs the WebhookHandler object.
   *
   * @param \Drupal\mysql\Driver\Database\mysql\Connection $connection
   *   The database driver connection.
   * @param \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher $event_dispatcher
   *   The event dispatcher.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler interface.
   * @param \Drupal\commerce_price\CurrencyFormatter $currency_formatter
   *   The Commerce currency formatter service.
   * @param \Drupal\commerce_gc_clinet\GoCardlessPartner $gocardless_partner
   *   The GoCardless partner service.
   */
  public function __construct(Connection $connection, ContainerAwareEventDispatcher $event_dispatcher, ModuleHandlerInterface $module_handler, CurrencyFormatter $currency_formatter, GoCardlessPartner $gocardless_partner) {
    $this->db = $connection;
    $this->eventDispatcher = $event_dispatcher;
    $this->moduleSettings = $this->config('commerce_gc_client.settings')->get();
    $this->logStorage = NULL;
    if ($module_handler->moduleExists('commerce_log')) {
      $this->logStorage = $this->entityTypeManager()->getStorage('commerce_log');
    }
    $this->currencyFormatter = $currency_formatter;
    $this->partner = $gocardless_partner;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('database'),
      $container->get('event_dispatcher'),
      $container->get('module_handler'),
      $container->get('commerce_price.currency_formatter'),
      $container->get('commerce_gc_client.gocardless_partner')
    );
  }

  /**
   * Capture and pre-process incoming webhooks from GoCardless.
   */
  public function webhook() {
    $headers = function_exists('getallheaders') ? getallheaders() : $this->getallheaders();
    $provided_signature = $headers['Webhook-Signature'];
    $mode = strpos($headers['Origin'], 'sandbox') !== FALSE ? 'sandbox' : 'live';

    if ($secret = $this->moduleSettings['webhook_secret_' . $mode]) {
      $webhook = file_get_contents('php://input');
      $calculated_signature = hash_hmac("sha256", $webhook, $secret);

      $hash_equals = function_exists('hash_equals') ? hash_equals($provided_signature, $calculated_signature) : $this->hashEquals($provided_signature, $calculated_signature);

      if ($hash_equals) {

        // Process the events.
        $data = json_decode($webhook, TRUE);

        // Webhooks can contain multiple events, but they are not ordered
        // sequentially and need to be re-ordered therefore. The following is
        // an improvement but does not deal with thousandths of a second so
        // some of the events may still be in the wrong order.  
        if (count($data['events']) > 1) {
          usort($data['events'], ['Drupal\commerce_gc_client\Controller\WebhookHandler','sortByTimestamp']);
        }

        $payments = [];
        $billing_requests = [];
        foreach ($data['events'] as $event) {

          switch ($event['resource_type']) {

            case 'mandates':
              if ($order_id = $this->getOrderId($event['links']['mandate'])) {
                if ($order = Order::load($order_id)) {
                  $this->mandates($order, $event);
                }
              }
              break;

            case 'subscriptions':
              if ($order_id = $this->getOrderId($event['links']['subscription'])) {
                if ($order = Order::load($order_id)) {
                  $this->subscriptions($order, $event);
                }
              }
              break;

            case 'payments':
              $payment = FALSE;
              $payment_id = $event['links']['payment'];

              // Since a webhook can include multiple events for the same
              // payment, remember it to avoid having to make repeated API
              // calls.
              if (!isset($payments[$payment_id])) {
                $result = $this->partner->api([
                  'mode' => $mode,
                  'endpoint' => 'payments',
                  'action' => 'get',
                  'id' => $payment_id,
                ]);
                if ($result && $result->response && $result->response->status_code == 200) {
                  $payment = $result->response->body->payments;
                  $payments[$payment_id] = $payment;
                }
              } 
              else {
                $payment = $payments[$payment_id];
              }

              if ($payment) {
                // If the payment was created via a recurring order. 
                if (isset($payment->metadata->recurring_order_id)) {
                  $order_id = $payment->metadata->recurring_order_id;
                }

                // Else payments can be received from orders that were created
                // through either a Redirect or a Billing Request flow.
                elseif (!$order_id = $this->getOrderId($payment->links->mandate)) {
                  if (isset($payment->metadata->order_id)) {
                    $order_id = $payment->metadata->order_id;
                  }
                }
                
                if (isset($order_id) && $order = Order::load($order_id)) {
                  $this->payments($order, $event, $payment);
                }
              }
              break;
            
            case 'billing_requests':
              if ($event['action'] == 'fulfilled') {
                $billing_request = FALSE;
                $billing_request_id = $event['links']['billing_request'];
                if (!isset($billing_requests[$billing_request_id])) {
                  $result = $this->partner->api([
                    'mode' => $mode,
                    'endpoint' => 'billing_requests',
                    'action' => 'get',
                    'id' => $billing_request_id,
                  ]);
                  if ($result && $result->response && $result->response->status_code == 200) {
                    $billing_request = $result->response->body->billing_requests;
                    $billing_requests[$billing_request_id] = $billing_request;
                  }
                } 
                else {
                  $billing_request = $billing_requests[$billing_request_id];
                }
                if ($billing_request) {
                  if (isset($billing_request->links->mandate_request_mandate)
                    && isset($billing_request->payment_request) 
                    && isset($billing_request->payment_request->metadata->order_id)) 
                  {
                    $order_id = $billing_request->payment_request->metadata->order_id;
                    $query = $this->db->select('commerce_gc_client', 'g')
                      ->condition('order_id', $order_id)
                      ->fields('g');

                    if (!$query->execute()->fetch()) {
                      $order = Order::load($order_id);
                      $this->billing_requests($order, $event, $billing_request);
                    }
                  }
                }
              }
              break;
          }

          if (isset($order)) {
            // Get payment gateway settings if not already set.
            if (!isset($this->paymentGatewaySettings)) {
              $payment_gateway_id = $order->payment_gateway->entity->Id();
              $this->paymentGatewaySettings = $this->config('commerce_payment.commerce_payment_gateway.' . $payment_gateway_id)->get();
            }

            // Adds a webhook event to the Drupal log if the feature is enabled
            // in the modules settings form.
            if ($this->paymentGatewaySettings['configuration']['log_webhook']) {
              if (!isset($this->logger)) {
                $this->logger = $this->getLogger('commerce_gc_client');
              }
              $this->logger->notice('<pre>GoCardless webhook: <br />' . print_r($event, TRUE) . '</pre>');
            }

            // Dispatch an event so that other modules can respond to webhook.
            !isset($resource) ? $resource = NULL : NULL;
            $webhook_event = new WebhookEvent($event, $resource, $order_id);
            $this->eventDispatcher->dispatch($webhook_event, GoCardlessEvents::WEBHOOK);
          }
        }
      }
      else {
        $this->logger = $this->getLogger('commerce_gc_client')->warning('Webhook cannot be processed because the webhook secret is invalid.');
        return new Response('Forbidden.', 403, ['Content-Type' => 'text/html']);
      }
    }
    else {
      $this->logger = $this->getLogger('commerce_gc_client')->warning('Webhook cannot be processed because the webhook secret is not set.');
      return new Response('Forbidden.', 403, ['Content-Type' => 'text/html']);
    }
    return new Response();
  }

  /**
   * Processes 'mandates' webhooks.
   *
   * @param object $order
   *   Commerce order entity.
   * @param array $event
   *   The webhook event as provided by GoCardless.
   */
  private function mandates($order, array $event) {
    if ($this->logStorage) {
      $this->logStorage->generate($order, 'webhook_description', [
        'description' => $event['details']['description'],
      ])->save();
    }

    $this->db->update('commerce_gc_client')->fields([
      'gc_mandate_status' => $event['action'],
    ])->condition('order_id', $order->id())->execute();
  }

  /**
   * Processes 'subscription' webhooks.
   *
   * @param object $order
   *   Commerce order entity.
   * @param array $event
   *   The webhook event as provided by GoCardless.
   */
  private function subscriptions($order, array $event) {
    if ($this->logStorage) {
      $this->logStorage->generate($order, 'webhook_description', [
        'description' => $event['details']['description'],
      ])->save();
    }

    if (!in_array($event['action'], ['payment_created', 'amended'])) {
      $this->db->update('commerce_gc_client_item')->fields([
        'gc_subscription_status' => $event['action'],
      ])->condition('gc_subscription_id', $event['links']['subscription'])->execute();
    }
  }

  /**
   * Processes 'payments' webhooks.
   *
   * @param object $order
   *   Commerce order entity.
   * @param array $event
   *   The webhook event as provided by GoCardless.
   * @param object $payment
   *   A GoCardless payment object that is retreived from via the API upon
   *   receipt of a 'payments' webhook.
   */
  private function payments($order, array $event, $payment) {
    if (!$this->paymentStorage) {
      $this->paymentStorage = $this->entityTypeManager()->getStorage('commerce_payment');
    }

    $commerce_payment_id = $this->db->select('commerce_payment', 'p')
      ->fields('p', ['payment_id'])
      ->condition('remote_id', $payment->id)
      ->execute()->fetchField();

    if ($event['action'] == 'created' && !$commerce_payment_id) {

      $total_price = $order->getTotalPrice();
      $currency_code = $total_price->getCurrencyCode();
      if ($currency_code != $payment->currency) {
        // Payment created through a Billing Request and applies to whole order
        if (isset($payment->metadata->order_id)) {
          $number = $total_price->getNumber();
          $price = new Price((string)$number, $currency_code);
        }
        // Payment created through any other means
        elseif (isset($payment->metadata->item_id)) {
          $item = $this->entityTypeManager()->getStorage('commerce_order_item')
            ->load($payment->metadata->item_id);
          $number = $item->getTotalPrice()->getNumber();
          $price = new Price((string)$number, $currency_code);
        }
        if ($this->logStorage) {
          $this->logStorage->generate($order, 'currency_exchanged', [
            'amount' => $this->currencyFormatter->format($payment->amount / 100, $payment->currency),
          ])->save();
        }
      }

      if (!isset($price)) {
        $price = new Price((string) ($payment->amount / 100), $payment->currency);
      }

      $this->paymentStorage->create([
        'state' => 'new',
        'amount' => $price,
        'payment_gateway' => $order->payment_gateway->entity->Id(),
        'order_id' => $order->id(),
        'remote_id' => $payment->id,
        'remote_state' => $payment->status,
      ])->save();
    }

    elseif ($commerce_payment_id) {
      $commerce_payment = Payment::load($commerce_payment_id);
      $commerce_payment_state = $commerce_payment->getState()->value;

      switch (TRUE) {
        case in_array($event['action'], [
          'pending_customer_approval',
          'pending_submission',
          'submitted',
        ]):
          if ($commerce_payment_state != 'completed') {
            $commerce_payment->setState('authorization');
          }
          break;

        case in_array($event['action'], [
          'confirmed',
          'paid_out',
        ]):
          $commerce_payment->setState('completed');
          break;

        case in_array($event['action'], [
          'cancelled',
          'customer_approval_denied',
          'failed',
          'charged_back',
        ]):
          $commerce_payment->setState('authorization_voided');
          if ($event['action'] == 'failed') {
            if ($this->logStorage) {
              $this->logStorage->generate($order, 'payment_failed', [
                'description' => $event['details']['description'],
              ])->save();
            }
          }
          break;
      }
      $commerce_payment->save();
    }
  }

  /**
   * Processes 'billing request' webhooks.
   *
   * @param object $order
   *   Commerce order entity.
   * @param array $event
   *   The webhook event as provided by GoCardless.
   * @param object $billing_request
   *   A GoCardless billing_request object that is retreived from via the API
   *   upon receipt of a 'billing_requests' webhook.
   */
  private function billing_requests($order, array $event, $billing_request) {
    // In case a mandate was requested during a billing_request_flow but was
    // not created before the customer returned to the site, then finish
    // processing the mandate now. 
    $mandate_id = $billing_request->links->mandate_request_mandate;

    foreach ($order->getItems() as $item) {
      $item_id = $item->id();
      if ($data = $item->getData('gc')) {

        if (!isset($gcid)) {
          $gcid = GoCardlessClient::processMandate($order, $billing_request, $mandate_id); 
        }

        $payment_gateway = $order->get('payment_gateway')->first()->entity;
        $partner = \Drupal::service('commerce_gc_client.gocardless_partner');
        $partner->setGateway($payment_gateway->id());
        $calculate = commerce_gc_client_price_calculate($order, $item);

        if ($gcid && $data['gc_type'] == 'P') {
          $payment_request = isset($billing_request->links->payment_request) ? true : false;
          GoCardlessClient::processPayment($payment_request, $partner, $item, $data, $calculate, $mandate_id, $gcid, false);
        }

        elseif ($gcid && $data['gc_type'] == 'S') {
          GoCardlessClient::processSubscription($partner, $item, $data, $calculate, $mandate_id, $gcid, false);
        }
      }
    }
  }

  /**
   * Get Commerce order ID.
   *
   * @param string $id
   *   Either a GoCardless mandate ID or a GoCardless subscription ID.
   *
   * @return int
   *   The Commerce order ID for the mandate or subscription.
   */
  private function getOrderId($id) {
    $query = $this->db->select('commerce_gc_client', 'g');
    if (substr($id, 0, 2) == 'MD') {
      $query->condition('gc_mandate_id', $id);
    }
    else {
      $query->join('commerce_gc_client_item', 'i', 'g.gcid = i.gcid');
      $query->condition('gc_subscription_id', $id);
    }
    return $query->fields('g', ['order_id'])->execute()->fetchField();
  }

  /**
   * Timing attack safe string comparison.
   *
   * Compares two strings using the same time whether they're equal or not.
   * Required when hash_equals() function is not present (PHP < 5.6.0).
   *
   * @param string $known_string
   *   The string of known length to compare against.
   * @param string $user_string
   *   The user-supplied string.
   *
   * @return bool
   *   Returns TRUE when the two strings are equal, FALSE otherwise.
   */
  private function hashEquals($known_string, $user_string) {
    $ret = 0;
    if (strlen($known_string) !== strlen($user_string)) {
      $user_string = $known_string;
      $ret = 1;
    }
    $res = $known_string ^ $user_string;
    for ($i = strlen($res) - 1; $i >= 0; --$i) {
      $ret |= ord($res[$i]);
    }
    return !$ret;
  }

  /**
   * Fetch all HTTP request headers.
   *
   * Required for Nginx servers that do not support getallheaders().
   *
   * @return mixed
   *   An associative array of all the HTTP headers in the current request, or
   *   FALSE on failure.
   */
  private function getallheaders() {
    $headers = [];
    foreach ($_SERVER as $name => $value) {
      if (substr($name, 0, 5) == 'HTTP_') {
        $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
      }
    }
    return $headers;
  }

  /**
   * usort() callback function.
   *
   * Sorts webhook events according to the created_at timestamp.
   */
  private static function sortByTimestamp($x, $y) {
    return strtotime($x['created_at']) - strtotime($y['created_at']);
  }

}

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

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