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;
}
