commerce_gc_client-8.x-1.9/commerce_gc_client.module

commerce_gc_client.module
<?php

/**
 * @file
 * Implements a GoCardless payment service for use with Drupal Commerce.
 */

use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\commerce_product\Entity\ProductVariationType;
use Drupal\commerce_order\Entity\Order;
use Drupal\commerce_order\Entity\OrderItem;
use Drupal\commerce_order\Entity\OrderType;
use Drupal\commerce_price\Entity\Currency;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Url;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\commerce_gc_client\Event\GoCardlessEvents;
use Drupal\commerce_gc_client\Event\PaymentCreatedEvent;
use Drupal\commerce_gc_client\Event\PaymentDetailsEvent;
use Drupal\commerce_gc_client\Event\PaymentNextEvent;

/**
 * Implements hook_help().
 */
function commerce_gc_client_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.commerce_gc_client':
      $url = 'https://seamless-cms.co.uk/commerce-installation-8';
      $output = '<p>' . t('For more information, and installation instructions for the module, visit:') . '</p>';
      $output .= '<p>' . t('<a target="new" href = @url>@url</a>', [
        '@url' => $url,
      ]) . '</p>';
      $output .= '<p>' . t('Or check out the README.md file that ships with the module.') . '</p>';
      return $output;
  }
}

/**
 * Assembles an array of data relevant to a payment creation.
 *
 * @param object $order
 *   Commerce order entity.
 * @param object $item
 *   Commerce order item entity.
 * @param object $payment
 *   Set if it is a scheduled payment and the function is called from cron.
 *   Not set if it is called during checkout.
 *
 * @return array
 *   An array of values relating to a payment, including the amount to be
 *   created and the currency code.
 */
function commerce_gc_client_price_calculate($order, $item, $payment = NULL) {
  $db = \Drupal::database();
  $adjs_arr = [];
  $adjs_total = 0;

  // Process scheduled adjustments.
  if ($payment) {
    $query = $db->select('commerce_gc_client_item', 'i');
    $query->join('commerce_gc_client_item_schedule', 's', 'i.item_id = s.item_id');
    $adjs = $query
      ->fields('s', ['sid', 'data', 'date', 'status'])
      ->condition('i.item_id', $item->id())
      ->condition('s.status', 1)
      ->condition('s.date', date('d M Y', $payment->next_payment))
      ->condition('s.type', 'adj')
      ->orderBy('timestamp', 'ASC')
      ->execute()->fetchAll();

    foreach ($adjs as $adj) {
      $adj_data = unserialize($adj->data);
      $adjs_arr[] = [$adj_data['title'] => $adj_data['amount']];
      $adjs_total = $adjs_total + $adj_data['amount'];
    }
  }

  $item_amount = $item->getTotalPrice()->getNumber();
  $amount = $item_amount + $adjs_total;

  // Include shipment costs if any.
  $data = $item->getData('gc');
  if (isset($data['shipment'])) {
    foreach ($order->getAdjustments() as $adj) {
      if ($adj->getType() == 'shipping') {
        $adj_amount = $adj->getAmount()->getNumber();
        $shipment_amount = round($adj_amount * $data['shipment']['proportion'], 2);
        $amount += $shipment_amount;
        break;
      }
    }
  }

  // Determine the scheme and the currency code for the payment.
  if ($payment) {
    $scheme = $payment->gc_mandate_scheme;
  }
  else {
    $scheme = $db->select('commerce_gc_client', 'g')
      ->fields('g', ['gc_mandate_scheme'])
      ->condition('order_id', $order->Id())
      ->execute()->fetchField();
  }

  $item_currency_code = $item->getTotalPrice()->getCurrencyCode();
  $payment_gateway_id = $order->payment_gateway->entity->Id();
  $settings = \Drupal::config('commerce_payment.commerce_payment_gateway.' . $payment_gateway_id)->get('configuration');

  // The scheme is already set because it is an existing order.
  if ($scheme) {
    foreach (\Drupal::config('commerce_gc_client.settings')->get('currency_schemes') as $gc_currency_code => $gc_currency) {
      if ($scheme == $gc_currency['scheme']) {
        $currency_code = $gc_currency_code;
        break;
      }
    }
  }

  // The order is being created and the scheme needs to be determined.  
  else {
    $country_code = $order->getBillingProfile()->address->country_code;
    
    foreach (\Drupal::config('commerce_gc_client.settings')->get('currency_schemes') as $gc_currency_code => $gc_currency) {
      // If payment gateway is configured to automatically select the country
      // for Instant Payments the scheme is resolved based on the customer's
      // billing address.
      if ($settings['instant_payments'] && $settings['countries']) {
        if (isset($gc_currency['countries'][$country_code])) {
          $currency_code = $gc_currency_code;
          $scheme = $gc_currency['scheme'];
          break;
        }
      }
      // Else the currency and scheme are resolved from the order item
      // currency code. 
      elseif ($gc_currency_code == $item_currency_code) {
        $currency_code = $gc_currency_code;
        $scheme = $gc_currency['scheme'];
        break;
      }
    }   
  } 

  // If the scheme is null at this point it is because customer is checking
  // out with an unsupported billing address country, or currency. 
  if (!$scheme) {
    $warning = t('Checkout with GoCardless not possible because currency or billing country is not supported.');
  }

  // Modify payment amount if order item currency does not match the determined 
  // currency.
  if ($item_currency_code !== $gc_currency_code) {
    $partner = \Drupal::service('commerce_gc_client.gocardless_partner');
    $partner->setGateway($payment_gateway_id);
    $result = $partner->api([
      'endpoint' => 'currency_exchange_rates',
      'action' => 'list',
      'source' => $item_currency_code,
      'target' => $gc_currency_code,
    ]);
    if ($result->response && $result->response->status_code == 200) {
      $exchange_rate = reset($result->response->body->currency_exchange_rates);
      $amount *= $exchange_rate->rate;
    }
    else {
      $warning = t('We were unable to obtain a currency exchange rate for your order, please contact the site administrator.');
    }
  }

  $calc = [
    'description' => 'Payment for Order #' . $order->id(),
    'line_item_amount' => $item_amount,
    'adjs' => isset($adjs_arr) ? $adjs_arr : NULL,
    'adjs_total' => $adjs_total,
    'amount' => number_format((float) $amount, 2, '.', ''),
    'exchange_rate' => isset($exchange_rate) ? $exchange_rate : NULL,
    'currency_code' => isset($currency_code) ? $currency_code : NULL,
    'country_code' => isset($country_code) ? $country_code : NULL,
    'scheme' => $scheme,
    'warning' => isset($warning) ? $warning : NULL,
  ];
  if (isset($shipment_amount)) {
    $calc += ['shipment' => $shipment_amount];
  }
  return $calc;
}

/**
 * Implements hook_cron().
 *
 * Loops through GoCardless items and if the next_payment date is in the
 * past attempts to create a payment, and if successful updates the
 * next_payment date based on the product's recurrence rules.
 */
function commerce_gc_client_cron() {
  // Postpone the operation if there is already an instance of
  // commerce_gc_client_cron running, to mitigate against the possibility of
  // extra payments being created in error. This can happen if cron takes more
  // than 240s to complete, and it is called again whilst still executing.
  if (!\Drupal::lock()->acquire('commerce_gc_client_cron', 3600)) {
    \Drupal::logger('commerce_gc_client')->warning('GoCardless payments could not be created becasue there is already an instance of cron running', []);
    return;
  }

  // Get list of active orders where next_payment is in the past.
  $db = \Drupal::database();
  $query = $db->select('commerce_gc_client', 'g');
  $query->join('commerce_gc_client_item', 'i', 'g.gcid=i.gcid');
  $payments = $query
    ->fields('g')
    ->fields('i')
    ->condition('i.type', ['S', 'P'], 'IN')
    ->condition('g.gc_mandate_status', 'cancelled', '!=')
    ->condition('i.next_payment', \Drupal::time()->getRequestTime(), '<=')
    ->execute()->fetchAll();

  if (!empty($payments)) {
    $event_dispatcher = \Drupal::service('event_dispatcher');
    $storage = \Drupal::entityTypeManager()->getStorage('commerce_payment');
    $uuid_service = \Drupal::service('uuid');
    $partner = \Drupal::service('commerce_gc_client.gocardless_partner');
  }

  foreach ($payments as $payment) {
    if (!$order = Order::load($payment->order_id)) {
      continue;
    }
    $partner->setGateway($order->payment_gateway->entity->Id());
    $mode = $payment->sandbox == 1 ? 'sandbox' : 'live';
    if (!$item = OrderItem::load($payment->item_id)) {
      continue;
    }

    $data = $item->getData('gc');
    $payment_valid = TRUE;

    // Retry a Subscription creation if there was a failure at the GoCardless
    // end during checkout.
    if ($payment->type == 'S') {
      $update_next_payment = TRUE;
      $update_array = ['next_payment' => NULL];
      if (isset($data['subs_details'])) {
        $result = $partner->api($data['subs_details']);

        // New subscription created successfully, or had already been created.
        if (isset($result->response) && in_array($result->response->status_code, [200, 201])) {
          $subscription = $result->response->body->subscriptions;
          $update_array = array_merge($update_array, [
            'gc_subscription_id' => $subscription->id,
          ]);
          unset($data['subs_details']);
          $item->setData('gc', $data)->save();
        }
        else {
          // If no response or response code 500 (Internal Server Error) then
          // do nothing and the Subscription creation will be retried on the
          // next cron run. But if there is a reponse then the error is at this
          // end so abort Subscription creation attempt.
          $log_array = ['@order_id' => $order->id()];
          if (isset($result->response) && $result->response && $result->response->status_code != 500) {
            $log = t('A scheduled subscription creation failed for order #@order_id.', $log_array);
            \Drupal::logger('commerce_gc_client')->error($log, []);
          }
          else {
            $update_next_payment = FALSE;
            $log = t('There was a problem with a scheduled subscription creation for order #@order_id, and it will be retried on the next cron run.', $log_array);
            \Drupal::logger('commerce_gc_client')->warning($log, []);
          }
        }
      }
      if ($update_next_payment) {
        $db->update('commerce_gc_client_item')->fields($update_array)
          ->condition('item_id', $payment->item_id)->execute();
      }
    }

    // Not a subscription. Make sure daily payment limit hasn't been exceeded.
    else {
      $payment_limit = $partner->settings['configuration']['payment_limit'];
      if ($payment_limit && $payment_limit != 0) {
        $result = $partner->api([
          'endpoint' => 'payments',
          'action' => 'list',
          'mode' => $mode,
          'mandate' => $payment->gc_mandate_id,
          'created_at_on_after' => date('c', strtotime('midnight')),
        ]);

        if ($result && $result->response->status_code != 200) {
          continue;
        }

        $previous_payments = $result->response->body->payments;
        $count = 0;
        foreach ($previous_payments as $previous) {
          if (in_array($previous->status, [
            'cancelled',
            'failed',
            'customer_approval_denied',
          ])) {
            continue;
          }
          if (isset($previous->metadata->item_id)) {
            $previous->metadata->item_id == $payment->item_id ? $count++ : NULL;
          }
        }

        if ($count >= $payment_limit) {
          $log = t(
          "Payment for order @order_id / customer @uid has not been created because the daily limit of @payment_limit payments has been exceeded.", [
            '@order_id' => $order->id(),
            '@uid' => $order->getCustomerId(),
            '@payment_limit' => $payment_limit,
          ]);
          \Drupal::logger('commerce_gc_client')->warning($log, []);

          // Send a warning email to admin.
          $mail_recipient = $partner->settings['configuration']['email_warnings'];
          $langcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
          $mail_params = [
            '@order_id' => $order->id(),
            '@payment_limit' => $payment_limit,
          ];
          \Drupal::service('plugin.manager.mail')
            ->mail('commerce_gc_client', 'payment-limit-reached', $mail_recipient, $langcode, $mail_params);
          // Todo need to do something better here.
          continue;
        }
      }

      // If payment details are in item's storage it is because they need to
      // be resubmitted with same idempotency key, so should not be altered by
      // another module and do not need validating.
      if (isset($data['payment_details'])) {
        $payment_details = $data['payment_details'];
      }
      else {
        $calculate = commerce_gc_client_price_calculate($order, $item, $payment);
        $payment_details = [
          'endpoint' => 'payments',
          'action' => 'create',
          'mode' => $mode,
          'mandate' => $payment->gc_mandate_id,
          'amount' => $calculate['amount'],
          'currency' => $calculate['currency_code'],
          'description' => $calculate['description'],
          'metadata' => [
            'item_id' => $payment->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, $payment->item_id, 'scheduled');
        $event_dispatcher->dispatch($payment_details_event, GoCardlessEvents::PAYMENT_DETAILS);
        if (!$payment_details = $payment_details_event->getPaymentDetails()) {
          continue;
        }

        // Validate the payment amount before creating.
        if ($payment_details['amount'] < 1 && $payment_details['amount'] != 0) {
          $amount = \Drupal::service('commerce_price.currency_formatter')->format((string) ($payment_details['amount']), $payment_details['currency']);
          $symbol = Currency::load($calculate['currency_code'])->getSymbol();
          $log = t(
            "Payment of @amount for order @order_id / customer @uid has not been created because it is less than @symbol1.", [
              '@symbol' => $symbol,
              '@amount' => $amount,
              '@order_id' => $order->id(),
              '@uid' => $order->getCustomerId(),
            ]
          );
          \Drupal::logger('commerce_gc_client')->warning($log, []);

          // Send a warning email to admin.
          $mail_recipient = $partner->settings['configuration']['email_warnings'];
          $langcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
          $mail_params = [
            '@order_id' => $order->id(),
            '@minimum' => $symbol . 1,
          ];
          \Drupal::service('plugin.manager.mail')
            ->mail('commerce_gc_client', 'payment-less-than-one', $mail_recipient, $langcode, $mail_params);
        }
        elseif ($payment_details['amount'] == 0) {
          $payment_valid = FALSE;
          $log = t(
            "No payment created for order @order_id / customer @uid because the amount is @symbol0.", [
              '@order_id' => $order->id(),
              '@uid' => $order->getCustomerId(),
              '@symbol' => Currency::load($calculate['currency_code'])->getSymbol(),
            ]
          );
          \Drupal::logger('commerce_gc_client')->warning($log, []);
        }
      }

      if ($payment_valid) {
          
        // Create a recurring order if required.
        if (isset($data['gc_create_order']) && $data['gc_create_order']) {
          $recurring_order = commerce_gc_client_recurring_order($order, $item, $data); 
          $payment_details['metadata']['recurring_order_id'] = $recurring_order->id();
          $payment_details['description'] = 'Payment for Order #' . $recurring_order->id();
        }

        // Create the payment.
        $result = $partner->api($payment_details);
        if (isset($result->response) && in_array($result->response->status_code, [200, 201])) {
          $payment_created = $result->response->body->payments;
          $amount = \Drupal::service('commerce_price.currency_formatter')->format((string) ($payment_details['amount']), $payment_details['currency']);
          $log = t("Payment of @amount for order #@order_id has been created with GoCardless and will be taken from customer #@uid's account on @charge_date.", [
            '@amount' => $amount,
            '@order_id' => $order->id(),
            '@uid' => $order->getCustomerId(),
            '@charge_date' => \Drupal::service('date.formatter')->format(strtotime($payment_created->charge_date), 'gocardless_client'),
          ]);
          \Drupal::logger('commerce_gc_client')->info($log, []);

          if (isset($data['payment_details'])) {
            unset($data['payment_details']);
            $item->setData('gc', $data)->save();
          }
          
          // If a recurring order has been created then save references to the
          // parent and child entities. 
          if (isset($data['gc_create_order']) && $data['gc_create_order']) {
            if ($gc_child = $recurring_order->getData('gc')) {
              $gc_child['gc_payment_id'] = $payment_created->id;
              $recurring_order->setData('gc', $gc_child)->save();
            }
            if (!$gc_parent = $order->getData('gc')) {
              $gc_parent = ['children' => []];
            }
            $gc_parent['children'][$payment_created->id] = $recurring_order->id();
            $order->setData('gc', $gc_parent)->save();
          }
        }

        else {
          $log_array = ['@order_id' => $order->id()];
          // If no response or response code 500 (Internal Server Error) then
          // save the payment_details array, so the payment will be retried
          // on the next cron run with the same idempotency key. But if there
          // is a reponse then the error is at this end so abort payment
          // creation attempt.
          if (!isset($result->response) || !$result->response || $result->response->status_code == 500) {
            $data['payment_details'] = $payment_details;
            $item->setData('gc', $data)->save();
            $log = t('There was a problem with a scheduled payment creation for order #@order_id, and it will be retried on the next cron run.', $log_array);
            \Drupal::logger('commerce_gc_client')->warning($log, []);
            continue;
          }
          else {
            if (isset($data['payment_details'])) {
              unset($data['payment_details']);
              $item->setData('gc', $data)->save();
            }
            $log = t('A scheduled payment could not be created for order #@order_id.', $log_array);
            \Drupal::logger('commerce_gc_client')->error($log, []);
          }
        }
      }

      // Update next_payment field in commerce_gc_client_item table.
      $next_payment = NULL;
      if (isset($data['interval_params'])) {
        $string = '+' . $data['interval_params']['string'];
        $next_payment = strtotime($string, $payment->next_payment);
      }

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

      $db->update('commerce_gc_client_item')
        ->fields(['next_payment' => $next_payment])
        ->condition('item_id', $payment->item_id)->execute();

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

      // Update status field in commerce_gc_client_schedules table.
      $db->update('commerce_gc_client_item_schedule')
        ->fields(['status' => 2])
        ->condition('type', 'adj')
        ->condition('date', date('d M Y', $payment->next_payment))
        ->condition('item_id', $payment->item_id)
        ->execute();
    }
  }
  \Drupal::lock()->release('commerce_gc_client_cron');
}

/**
 * Creates a recurring order upon creation of a recurring payment.
 *
 * @param object $order
 *   The Commerce Order entity.
 * @param object $item
 *   The Commerce Order Item entity.
 * @param array $data
 *   The Commerce Order Item data array.
 *
 * @return commerce
 *   A Commerce Order entity object.
 */
function commerce_gc_client_recurring_order($order, $item, $data) {
  $recurring_item = OrderItem::create([
    'type' => $item->bundle(),
    'title' => $item->getTitle(),
    'purchased_entity' => $item->getPurchasedEntityId(),
    'quantity' => $item->getQuantity(),
    'unit_price' => $item->getUnitPrice(),
    //'overridden_unit_price' => $item->isUnitPriceOverridden(),
    'overridden_unit_price' => true,
  ]);
  $recurring_item->setData('gc', $data);
  $recurring_item->save();

  $state = isset($data['gc_email_invoice']) && $data['gc_email_invoice'] ? 'draft' : 'completed';

  $order_params = [
    'type' => $order->bundle(),
    'mail' => $order->getEmail(),
    'uid' => $order->getCustomerId(),
    'store_id' => $order->getStoreId(),
    'billing_profile' => $order->getBillingProfile(),
    'order_items' => [$recurring_item],
    'payment_gateway' => $order->payment_gateway->entity->Id(),
    'state' => $state,
  ];
  if ($state == 'completed') {
    $order_params['placed'] = \Drupal::time()->getCurrentTime();
  }
  $recurring_order = Order::create($order_params);
  
  // Reference the parent order in the new order's data.
  $recurring_order->setData('gc', [
    'parent' => $order->id(),
    'item_id' => $item->id(),
  ]);

  $recurring_order->save();

  // Provide the new order with an order number.
  if (!$recurring_order->getOrderNumber()) {
    $order_type = \Drupal::entityTypeManager()->getStorage('commerce_order_type')
      ->load($recurring_order->bundle());
    $number_pattern = $order_type->getNumberPattern();
    if ($number_pattern) {
      $order_number = $number_pattern->getPlugin()->generate($recurring_order);
    }
    else {
      $order_number = $recurring_order->id();
    }
    $recurring_order->setOrderNumber($order_number);
    $recurring_order->save();
  }

  // "Placing" the order means that the Order Confirmation email is sent. 
  if ($state = 'draft') {
    $recurring_order->getState()->applyTransitionById('place');
    $recurring_order->save();
  }

  // Provide order log entries with links to parent / child.
  if (\Drupal::service('module_handler')->moduleExists('commerce_log')) {
    $log_storage = \Drupal::entityTypeManager()->getStorage('commerce_log');
    $log_storage->generate($recurring_order, 'recurring_order', [
      'link_to_parent' => $order->toLink('order #' . $order->id()),
    ])->save();
    $log_storage->generate($order, 'parent_order', [
      'link_to_child' => $recurring_order->toLink('Order #' . $recurring_order->id()),
    ])->save();
  }

  return $recurring_order;
}

/**
 * Implements hook_preprocess_HOOK() for commerce-order-receipt.html.twig.
 */
function commerce_gc_client_preprocess_commerce_order_receipt(&$variables) {
  // If it is a recurring order, then add some extra variables to the
  // commerce-order-receipt.html.twig template so themers can include 
  // information about the parent order. 
  $order = $variables['order_entity'];
  if ($gc = $order->getData('gc')) {
    if (isset($gc['parent'])) {
      if ($parent_order = Order::load($gc['parent'])) {
        $admin_url = $parent_order->toLink()->getUrl();
        $admin_url->setAbsolute();
        $user_url = Url::fromRoute('entity.commerce_order.user_view', [
          'commerce_order' => $parent_order->id(),
          'user' => $parent_order->getCustomerId(),
        ]);
        $user_url->setAbsolute();
        $variables['gc_parent'] = [
          'order_id' => $gc['parent'],
          'url' => [
            'admin' => $admin_url->toString(),
            'user' => $user_url->toString(),
          ],
        ];
      }
    }
  }
}

/**
 * Implements hook_mail().
 */
function commerce_gc_client_mail($key, &$message, $params) {
  $base_url = \Drupal::request()->getSchemeAndHttpHost();
  $order_url = $base_url . '/admin/commerce/orders/' . $params['@order_id'];
  $link = t('order <a href="@url">#@order_id</a>', [
    '@order_id' => $params['@order_id'],
    '@url' => $order_url,
  ]);
  $message_arr = [
    '@link' => $link,
    '@order_url' => $order_url,
  ];
  foreach ($params as $param_key => $value) {
    $message_arr[$param_key] = $value;
  }
  $message['headers']['Content-Type'] = 'text/html; charset=UTF-8; format=flowed';

  switch ($key) {
    case 'payment-less-than-one':
      $message['subject'] = t("A payment's amount was less than @minimum", $message_arr);
      $message['body'][] = '<p>' . t("A payment for order <a href='@order_url'>#@order_id</a> has not been created because it's amount was less than @minimum.", $message_arr) . '</p>';
      break;

    case 'payment-limit-reached':
      $message['subject'] = t("The daily payment limit has been exceeded for an order!");
      $message['body'][] = '<p>' . t("A payment for order <a href='@order_url'>#@order_id</a> has not been created because the daily limit of @payment_limit payments has been exceeded.", $message_arr) . '</p>';
      break;
  }
}

/**
 * Returns and array of GoCardless enabled three digit currency codes.
 *
 * @return array
 *   Array of three digit, anabled currency codes.
 */
function commerce_gc_client_enabled_currencies() {
  $gc_currencies = \Drupal::config('commerce_gc_client.settings')->get('currency_schemes');
  $enabled_gc_currencies = [];
  foreach ($gc_currencies as $gc_currency_code => $gc_currency) {
    if (isset($gc_currency['enabled']) && $gc_currency['enabled']) {
      $enabled_gc_currencies[] = $gc_currency_code;
    }
  }
  return $enabled_gc_currencies;
}

/**
 * Retrieves module specific data for a commerce product variation.
 *
 * @param int $variation_id
 * 
 * @return null|object
 */
function commerce_gc_client_get_variation_data($variation_id) {
  $db = \Drupal::service('database');
  return $db->select('commerce_gc_client_variation', 'v')
    ->fields('v')
    ->condition('variation_id', $variation_id)
    ->condition('gc_use', true)
    ->execute()->fetch();
}

/**
 * Retrieves module specific data for a commerce order item.
 *
 * @param int $item_id
 * 
 * @return null|object
 */
function commerce_gc_client_get_order_item_data($item_id) {
  $db = \Drupal::service('database');
  $query = $db->select('commerce_gc_client', 'g');
  $query->join('commerce_gc_client_item', 'i', 'g.gcid=i.gcid');
  return $query
    ->fields('g')
    ->fields('i')
    ->condition('item_id', $item_id)
    ->execute()->fetch();
}

/**
 * Implements hook_inline_entity_form_entity_form_alter().
 *
 * Filters available currencies in inline Product Variation forms according to
 * GoCardless settings.
 */
function commerce_gc_client_inline_entity_form_entity_form_alter(&$entity_form, &$form_state) {
  if ($entity_form['#entity_type'] == 'commerce_product_variation') {
    $variation_id = $entity_form['#entity']->id();
    if (commerce_gc_client_get_variation_data($variation_id)) {
      $entity_form['price']['widget'][0]['#available_currencies'] = commerce_gc_client_enabled_currencies();
    }
  }
}

/**
 * Implements hook_field_widget_form_alter().
 *
 * Filters available currencies in Product Variation Edit forms according to
 * GoCardless settings.
 */
function commerce_gc_client_field_widget_form_alter(&$element, FormStateInterface $form_state, $context) {
  $base_id = $context['widget']->getBaseId();
  if (in_array($base_id, ['commerce_price_default', 'commerce_list_price'])) {
    $form_id = $form_state->getBuildInfo()['form_id'];
    if (strpos($form_id, 'commerce_product_variation_') !== FALSE
        ||  (strpos($form_id, 'commerce_product_') !== FALSE)
    ) {
      $variation_id = $form_state->getFormObject()->getEntity()->id();
      if ($base_id == 'commerce_price_default' && commerce_gc_client_get_variation_data($variation_id)) {
        $element['#available_currencies'] = commerce_gc_client_enabled_currencies();
      }
      elseif ($base_id == 'commerce_list_price' && commerce_gc_client_get_variation_data($variation_id)) {
        $element['value']['#available_currencies'] = commerce_gc_client_enabled_currencies();
      }
    }
  }
}

/**
 * Implements hook_form_BASE_FORM_ID_alter() for Product Variation Type entities.
 *
 * Provides a setting for enabling GoCardless recurrence rules with a
 * product variation type.
 */
function commerce_gc_client_form_commerce_product_variation_type_form_alter(&$form, FormStateInterface $form_state) {
  // Add a checkbox to product variation "Add" and "Edit" forms to determine
  // if the variation type uses GoCardless recurrence rules or not.
  $product_variation_type = $form_state->getFormObject()->getEntity()->id();
  $gc_product_variation_types = \Drupal::config('commerce_gc_client.settings')->get('product_variation_types');
  $form['gc'] = [
    '#type' => 'checkbox',
    '#default_value' => in_array($product_variation_type, $gc_product_variation_types) ? TRUE : FALSE,
    '#title' => t("Use GoCardless recurrence rules with this product variation type."),
  ];
  $form['actions']['submit']['#submit'][] = 'commerce_gc_client_product_variation_type_submit';
}

/**
 * Form submit function.
 *
 * Saves a setting for using GoCardless recurrence rules with a product
 * variation type.
 */
function commerce_gc_client_product_variation_type_submit(&$form, FormStateInterface $form_state) {
  $product_variation_type = $form_state->getValue('id');
  $settings = \Drupal::configFactory()->getEditable('commerce_gc_client.settings');
  $gc_product_variation_types = $settings->get('product_variation_types');

  if ($form_state->getValue('gc') && !array_search($product_variation_type, $gc_product_variation_types)) {
    $gc_product_variation_types[] = $product_variation_type;
  }
  else {
    if (($key = array_search($product_variation_type, $gc_product_variation_types)) !== FALSE) {
      unset($gc_product_variation_types[$key]);
    }
  }
  $settings->set('product_variation_types', $gc_product_variation_types)->save();
}

/**
 * Implements hook_ENTITY_TYPE_delete().
 *
 * Disconnects site from GoCardless Partner upon deletion of GoC payment
 * gateway.
 */
function commerce_gc_client_commerce_payment_gateway_delete(EntityInterface $entity) {
  if ($entity->getPluginId() == 'gocardless_client') {
    $partner = \Drupal::service('commerce_gc_client.gocardless_partner');
    $partner->setGateway($entity->id());
    foreach (['sandbox', 'live'] as $mode) {
      $auth = $partner->api(['mode' => $mode]);
      if (is_int($auth) && $auth == 200) {
        $result = $partner->api([
          'endpoint' => 'oauth',
          'action' => 'revoke',
        ]);

        if ($result->response == 200) {
          // TODO ensure there are no other GoC payment gateways enabled before
          // doing this.
          $session = \Drupal::request()->getSession();
          if ($session->get('commerce_gc_client_cookie_created')) {
            $session->remove('commerce_gc_client_cookie_created');
          }
          $config = \Drupal::service('config.factory')->getEditable('commerce_gc_client.settings');
          $config->set('partner_user_' . $mode, NULL)->save();
          $config->set('partner_pass_' . $mode, NULL)->save();
          \Drupal::messenger()->addWarning(t('You have been disconnected from GoCardless @mode.', ['@mode' => $mode]));
        }
        else {
          \Drupal::messenger()->addError(t('There was a problem disconnecting you from GoCardless @mode.', ['@mode' => $mode]));
        }
      }
    }
  }
}

/**
 * Implements hook_ENTITY_TYPE_delete().
 *
 * Removes a Product variation type from the list of GoCardless enabled
 * variation types.
 */
function commerce_gc_client_commerce_product_variation_type_delete(EntityInterface $entity) {
  $product_variation_type = $entity->id();
  $settings = \Drupal::configFactory()->getEditable('commerce_gc_client.settings');
  $gc_product_variation_types = $settings->get('product_variation_types');
  if (($key = array_search($product_variation_type, $gc_product_variation_types)) !== FALSE) {
    unset($gc_product_variation_types[$key]);
  }
  $settings->set('product_variation_types', $gc_product_variation_types)->save();
}

/**
 * Implements hook_entity_extra_field_info().
 *
 * Adds the "Product information".
 */
function commerce_gc_client_entity_extra_field_info() {
  $extra = [];
  $gc_variation_types = \Drupal::config('commerce_gc_client.settings')->get('product_variation_types');
  foreach (ProductVariationType::loadMultiple() as $bundle) {
    if (in_array($bundle->id(), $gc_variation_types)) {
      $extra['commerce_product_variation'][$bundle->id()] = [
        'form' => [
          'recurrence_rules' => [
            'label' => t('GoCardless Recurrence Settings'),
            'description' => t('Subform for setting product variation recurrence rules.'),
            'weight' => 10,
            'visible' => TRUE,
          ],
        ],
      ];
    }
  } 

  foreach (OrderType::loadMultiple() as $bundle) {
    $extra['commerce_order'][$bundle->id()] = [
      'display' => [
        'commerce_gc_client_mandate_info' => [
          'label' => t('GoCardless mandate information'),
          'description' => t('Extra GoCardless information for an order.'),
          'weight' => 10,
          'visible' => TRUE,
        ],
        'commerce_gc_client_mandate_cancel' => [
          'label' => t('GoCardless mandate cancel button'),
          'description' => t('Mandate cancellation link for customer view.'),
          'weight' => 20,
          'visible' => TRUE,
        ],
      ],
    ];
  }
  return $extra;
}

/**
 * Implements hook_ENTITY_TYPE_view().
 *
 * Add extra GoCardless information and cancellation button to order
 * view pages.
 *
 * If additional content does not display see:
 * https://www.drupal.org/project/commerce/issues/2915559
 */
function commerce_gc_client_commerce_order_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
  $db = \Drupal::database();
  $gc = $db->select('commerce_gc_client', 'g')
    ->fields('g')
    ->condition('order_id', $entity->id())
    ->execute()->fetch();

  if (!$gc) {
    return;
  }

  // Extra information.
  if ($display->getComponent('commerce_gc_client_mandate_info')) {

    $header = [[
      'data' => t('GoCardless mandate details'),
      'colspan' => 2,
    ],
    ];

    $data = unserialize($gc->data);
    $rows = [
      [t('Mandate ID:'), $gc->gc_mandate_id],
      [t('Customer ID:'), $data['gc_customer_id']],
      [t('Status:'), ucfirst($gc->gc_mandate_status)],
      [t('Scheme:'), $gc->gc_mandate_scheme],
      [t('Created date:'),
        \Drupal::service('date.formatter')->format($gc->created,
        'gocardless_client'),
      ],
    ];

    if ($view_mode == 'admin') {
      $rows[] = [
        t('Environment:'),
        $gc->sandbox == 1 ? t('Sandbox') : t('Live'),
      ];
    }

    $build['commerce_gc_client_mandate_info'] = [
      '#type' => 'table',
      '#header' => $header,
      '#rows' => $rows,
      '#prefix' => '<div id="gc-mandate-details">',
      '#suffix' => '</div>',
      '#attached' => ['library' => ['commerce_gc_client/gocardless-client']],
    ];
  }

  // Cancellation button.
  if ($display->getComponent('commerce_gc_client_mandate_cancel')) {
    if ($gc->gc_mandate_status == 'cancelled') {
      return;
    }
    $uid = $entity->getCustomer()->id();
    $path = '/user/' . $uid . '/orders/' . $entity->id() . '/mandate_cancel/' . $view_mode;
    $markup = '<div style="margin-top: 20px;">' . t('<a class="button" href="@path">Cancel mandate</a>', ['@path' => $path]) . '</div>';
    $build['commerce_gc_client_mandate_cancel'] = [
      '#markup' => $markup,
    ];

    // Disbale the Twig cache for the user view so that when the user is
    // returned to the page the cancellation is apparent.
    if ($view_mode == 'user') {
      $build['#cache']['max-age'] = 0;
    }
  }
}

/**
 * Cancels a GoCardless mandate.
 *
 * @param int $order_id
 *   The Commerce order ID for the mandate that is being cancelled.
 * @param object|null $mandate
 *   The commerce_gc_client database table object, for the mandate that is
 *   being cancelled, or null. If null the mandate(s) are obtained from the
 *   database using the Commerce order ID.
 */
function commerce_gc_client_mandate_cancel($order_id, $mandate = NULL) {
  $db = \Drupal::database();
  if ($mandate) {
    $mandates[$mandate->gcid] = $mandate;
  }
  else {
    $mandates = $db->select('commerce_gc_client', 'c')
      ->fields('c', ['gcid', 'gc_mandate_id', 'sandbox'])
      ->condition('order_id', $order_id)
      ->condition('gc_mandate_status', 'cancelled', '!=')
      ->execute()->fetchAllAssoc('gcid');
  }
  if (empty($mandates)) {
    return;
  }

  $partner = \Drupal::service('commerce_gc_client.gocardless_partner');
  foreach ($mandates as $gcid => $mandate) {
    $result = $partner->api([
      'endpoint' => 'mandates',
      'action' => 'cancel',
      'mandate' => $mandate->gc_mandate_id,
      'mode' => $mandate->sandbox ? 'sandbox' : 'live',
    ]);

    if (!isset($result->response)) {
      \Drupal::messenger()->addError(t('Something went wrong cancelling GoCardless debit mandate @mandate. Please try again or contact the site administrator for assistance.', ['@mandate' => $mandate->gc_mandate_id]));
    }
    elseif ($result->response->status_code == 200) {
      \Drupal::messenger()->addMessage(t('Your GoCardless debit mandate @mandate has been cancelled, and you will receive an email confirmation.', ['@mandate' => $mandate->gc_mandate_id]));

      $db->update('commerce_gc_client')->fields([
        'gc_mandate_status' => 'cancelled',
      ])->condition('order_id', $order_id)->execute();

      $db->update('commerce_gc_client_item')->fields([
        'next_payment' => NULL,
      ])->condition('gcid', $gcid)->execute();

      $items = $db->select('commerce_gc_client_item', 'i')
        ->fields('i')
        ->condition('gcid', $gcid)
        ->execute()->fetchAllAssoc('item_id');

      foreach ($items as $item_id => $item) {
        $db->update('commerce_gc_client_item_schedule')->fields([
          'status' => 0,
        ])->condition('item_id', $item_id)->execute();
      }
    }
  }
}

/**
 * Implements hook_commerce_order_item_insert().
 */
function commerce_gc_client_commerce_order_item_insert(EntityInterface $item) {
  // If the item is created through the admin order interface and it's
  // purchased entity is configurred to use GoCardless recurring rules, then
  // add item data to the commerce_gc_client_item table.  
  if (\Drupal::routeMatch()->getRouteName() == 'entity.commerce_order.edit_form') {
    if (!$item->getPurchasedEntity()) {
      return;
    }

    $variation_id = $item->getPurchasedEntity()->id();
    if ($result = commerce_gc_client_get_variation_data($variation_id)) {
      $gc = unserialize($result->data);
      $interval_params = [];
      if ($gc['gc_interval_length']) {
        $interval_params['length'] = $gc['gc_interval_length'];
        $interval_params['unit'] = $gc['gc_interval_unit'];
      }
      if ($interval_params) {
        $interval_params['string'] = $interval_params['length'] . ' ' . str_replace("ly", "", $interval_params['unit']);
        $interval_params['gc'] = $interval_params['length'] . ' ' . $interval_params['unit'];
        $gc['interval_params'] = $interval_params;
      }
      $item->setData('gc', $gc)->save();

      $db = \Drupal::database();
      if ($gcid = $db->select('commerce_gc_client', 'g')
        ->fields('g', ['gcid'])
        ->condition('order_id', $item->getOrderId())
        ->execute()->fetchField() 
      ) {
        $fields = [
          'item_id' => $item->id(),
          'gcid' => $gcid,
          'type' => $gc['gc_type'],
        ];
        $db->insert('commerce_gc_client_item')->fields($fields)->execute();
        
        if ($gc['gc_type'] == 'P') {
          \Drupal::messenger()->addWarning(t("A new order item has been created, please use the GoCardless tab to set the 'Next Scheduled Payment' date manually."));
        }
        else {
          \Drupal::messenger()->addWarning(t("A new order item has been created but it has not been possible to create a Subscription with GoCardless."));
        }
      }
    }
  }
}

/**
 * Implements hook_commerce_order_item_delete().
 *
 * Unset the next scheduled payment to prevent errors during cron.
 */
function commerce_gc_client_commerce_order_item_delete(EntityInterface $item) {
  if ($item instanceof OrderItem) {
    $db = \Drupal::database();
    $db->update('commerce_gc_client_item')
      ->fields(['next_payment' => NULL])
      ->condition('item_id', $item->id())->execute();
  }
}

/**
 * Implements hook_commerce_order_delete().
 *
 * Remove GoCardless details from database when deleting an order.
 */
function commerce_gc_client_commerce_order_delete(EntityInterface $order) {
  $db = \Drupal::database();
  $mandate = $db->select('commerce_gc_client', 'g')
    ->fields('g')
    ->condition('order_id', $order->id())
    ->execute()->fetch();

  if (!empty($mandate)) {

    // Cancel GoCardless mandate first before deleting the order.
    if ($mandate->gc_mandate_status !== 'cancelled') {
      commerce_gc_client_mandate_cancel($order->id(), $mandate);
    }

    foreach ($order->getItems() as $item) {
      $db->delete('commerce_gc_client_item_schedule')->condition('item_id', $item->id())->execute();
      $db->delete('commerce_gc_client_item')->condition('item_id', $item->id())->execute();
    }

    $db->delete('commerce_gc_client')->condition('gcid', $mandate->gcid)->execute();
  }
}

/**
 * Implements hook_menu_local_tasks_alter().
 *
 * Remove GoCardless menu tab from order admin pages if the order was not
 * made through this service.
 */
function commerce_gc_client_menu_local_tasks_alter(&$data, $route_name) {
  if (isset($data['tabs'][0]['commerce_gc_client.mandates_form'])) {
    $current_route = \Drupal::routeMatch();
    $order = $current_route->getParameters()->get('commerce_order');
    if ($order && is_object($order)) {
      if (!$order->getData('gc') && (!$order->payment_gateway->entity || $order->payment_gateway->entity->getPluginId() != 'gocardless_client')) {
        unset($data['tabs'][0]['commerce_gc_client.mandates_form']);
      }
    }
  }
}

/**
 * Implements hook_entity_operation().
 */
function commerce_gc_client_entity_operation(EntityInterface $entity) {
  $operations = [];
  $type = $entity->getEntityTypeId();
  
  if ($type == 'commerce_order') {
    // Do not show for a cart order.
    if ($entity->hasField('cart') && $entity->get('cart')->value) {
      return;
    }
    // Only show if the user has the "administer commerce_order" permission.
    if (!\Drupal::currentUser()->hasPermission('administer commerce_order')) {
      return;
    }
    $operations['gocardless'] = [
      'title' => t('GoCardless'),
      'url' => Url::fromRoute('commerce_gc_client.mandate', [
        'commerce_order' => $entity->id(),
      ]),
      'weight' => 60,
    ];
  }

  elseif ($type == 'commerce_product_variation') {
    $gc_variation_types = \Drupal::config('commerce_gc_client.settings')->get('product_variation_types');
    if (in_array($entity->bundle(), $gc_variation_types)) {
      $url = Url::fromRoute('commerce_gc_client.product_variation.recurrence_rules', [
        'commerce_product' => $entity->getProduct()->id(),
        'commerce_product_variation' => $entity->id(),
      ]);
      $operations['recurrence_rules'] = [
        'title' => t('Recurrence Rules'),
        'url' => $url,
        'weight' => 100,
      ];  
    }
  }
  return $operations;
}

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

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