commerce_gc_client-8.x-1.9/src/Form/Mandate.php
src/Form/Mandate.php
<?php
namespace Drupal\commerce_gc_client\Form;
use Drupal\commerce_gc_client\GoCardlessPartner;
use Drupal\commerce_gc_client\Event\GoCardlessEvents;
use Drupal\commerce_gc_client\Event\PaymentCreatedEvent;
use Drupal\commerce_price\Entity\Currency;
use Drupal\commerce_price\CurrencyFormatter;
use Drupal\commerce_order\Entity\Order;
use Drupal\commerce_order\Entity\OrderItem;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Utility\Html;
use Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Ajax\RedirectCommand;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Url;
use Drupal\mysql\Driver\Database\mysql\Connection;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Defines a form for managing GoCardless mandates for order items.
*/
class Mandate extends FormBase {
/**
* The current Request Stack.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $currentRequest;
/**
* The database driver connection.
*
* @var \Drupal\mysql\Driver\Database\mysql\Connection
*/
protected $db;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* The currency formatter service.
*
* @var Drupal\commerce_price\CurrencyFormatter
*/
protected $currencyFormatter;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $config;
/**
* The event dispatcher.
*
* @var \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher
*/
protected $eventDispatcher;
/**
* The commerce order ID for the form.
*
* @var int
*/
protected $orderId;
/**
* The modules's GoCardless IDs for the order items.
*
* @var array
*/
protected $gcs;
/**
* The GoCardless Partner service.
*
* @var \Drupal\commerce_gc_client\GoCardlessPartner
*/
protected $partner;
/**
* The currency code for the Commerce order.
*
* @var string
*/
protected $commerceCurrencyCode;
/**
* The module's currency definition for the Commerce order.
*
* @var array
*/
protected $gcCurrency;
/**
* The GoCardless mandate's currency code.
*
* @var string
*/
protected $currencyCode;
/**
* List of items contained in order for select list.
*
* @var array
*/
protected $itemSelect;
/**
* The selected Commerce order item ID.
*
* @var int
*/
protected $itemId;
/**
* The modules's GoCardless data for the selected order item.
*
* @var object
*/
protected $gc;
/**
* The GoCardless.com mandate for the order.
*
* @var object
*/
protected $mandate;
/**
* The selected Commerce order item.
*
* @var Drupal\commerce_order\Entity\OrderItem
*/
protected $item;
/**
* The default amount for the selected order item.
*
* @var string
*/
protected $defaultAmount;
/**
* The default name for the selected order item.
*
* @var string
*/
protected $defaultName;
/**
* The time service.
*
* @var \Drupal\Component\Datetime\TimeInterface
*/
protected $time;
/**
* Constructs a new Mandate object.
*
* @param \GuzzleHttp\Client $current_request
* The GuzzleHttp client service.
* @param \Drupal\mysql\Driver\Database\mysql\Connection $connection
* The database driver connection.
* @param \Drupal\commerce_price\CurrencyFormatter $currencyFormatter
* The Commerce currency formatter service.
* @param \Drupal\Core\Datetime\DateFormatterInterface $dateFormatter
* The date formatter service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The config factory.
* @param \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher $eventDispatcher
* The event dispatcher.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \Drupal\commerce_gc_clinet\GoCardlessPartner $goCardlessPartner
* The GoCardless partner service.
*/
public function __construct(Request $current_request, Connection $connection, CurrencyFormatter $currencyFormatter, DateFormatterInterface $dateFormatter, ConfigFactoryInterface $configFactory, ContainerAwareEventDispatcher $eventDispatcher, TimeInterface $time, GoCardlessPartner $goCardlessPartner) {
$this->currentRequest = $current_request;
$this->db = $connection;
$this->currencyFormatter = $currencyFormatter;
$this->dateFormatter = $dateFormatter;
$this->config = $configFactory->get('commerce_gc_client.settings')->get();
$this->eventDispatcher = $eventDispatcher;
$this->time = $time;
$this->partner = $goCardlessPartner;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('request_stack')->getCurrentRequest(),
$container->get('database'),
$container->get('commerce_price.currency_formatter'),
$container->get('date.formatter'),
$container->get('config.factory'),
$container->get('event_dispatcher'),
$container->get('datetime.time'),
$container->get('commerce_gc_client.gocardless_partner')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'commerce_gc_client_mandate_form';
}
/**
* Form constructor.
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$order = $this->getRouteMatch()->getParameter('commerce_order');
// If it is a recurring order, then use the parent order instead, but only
// use the relevant order item.
$is_recurring = FALSE;
if ($order_gc = $order->getData('gc')) {
if (isset($order_gc['parent'])) {
$order = Order::load($order_gc['parent']);
$item = OrderItem::load($order_gc['item_id']);
$order->setItems([$item]);
$is_recurring = 'child';
}
else {
$is_recurring = 'parent';
}
}
$payment_gateway_id = $order->get('payment_gateway')->first()->entity->Id();
$this->partner->setGateway($payment_gateway_id);
$this->orderId = $order->id();
$query = $this->db->select('commerce_gc_client', 'g');
$query->join('commerce_gc_client_item', 'i', 'i.gcid=g.gcid');
$this->gcs = $query
->fields('g')
->fields('i')
->condition('order_id', $this->orderId)
->execute()
->fetchAllAssoc('item_id');
$count = $is_recurring == 'child' ? 1 : count($this->gcs);
if ($count == 0) {
$message = $this->t('You have no GoCardless mandates associated with this order.');
$payment_gateway_id = $order->payment_gateway->entity->Id();
if ($order->payment_gateway->entity->getPluginId() == 'gocardless_client') {
$message .= $this->t(' Check the "Payments" tab for details of Instant Payments.');
}
$this->messenger()->addWarning($message);
return;
}
if ($total_price = $order->getTotalPrice()) {
$this->commerceCurrencyCode = $total_price->getCurrencyCode();
$commerce_currency_symbol = Currency::load($this->commerceCurrencyCode)->getSymbol();
}
foreach ($this->config['currency_schemes'] as $this->currencyCode => $this->gcCurrency) {
if ($this->gcCurrency['scheme'] == reset($this->gcs)->gc_mandate_scheme) {
$currency_symbol = Currency::load($this->currencyCode)->getSymbol();
break;
}
}
// Create an array of unique line item names.
$this->itemSelect = [];
$title_count = [];
$items = $order->getItems();
foreach ($this->gcs as $gc) {
$item = OrderItem::load($gc->item_id);
$select_id = $gc->item_id;
$item_title = $item ? $item->getTitle() : $gc->item_id;
!isset($title_count[$item_title]) ? $title_count[$item_title] = 1 : $title_count[$item_title]++;
$title[$select_id] = $title_count[$item_title] == 1 ? $item_title : $item_title . ' (' . $title_count[$item_title] . ')';
$this->itemSelect[$select_id] = $title[$select_id];
}
if ($count == 1) {
$this->itemId = reset($this->gcs)->item_id;
$this->item = reset($items);
$item_title = isset($item_title) ? $item_title : t('Unknown');
if ($is_recurring == 'child') {
$params = ['@link' => $order->toLink('Order #'. $order->id())->toString()];
$text_top = t('<h3><b>Created as a recurring payment from @link</b></h3>', $params);
}
else {
$text_top = t('<h3><b>Administrate @title</b></h3>', [
'@title' => $item_title,
]);
}
}
else {
if ($item_id = $this->currentRequest->query->get('item_id')) {
$this->itemId = $item_id;
}
else {
$this->itemId = reset($this->gcs)->item_id;
}
$this->item = OrderItem::load($this->itemId);
$form = ['#attributes' => ['id' => ['switch-item']]];
$text_top = t('<h3><b>Administrate @title</b></h3>', [
'@title' => $this->itemSelect[$this->itemId],
]);
}
if (!$this->item) {
$text_top .= t('<div class="messages messages--warning">This order item appears to have been deleted.</div>');
}
$form['text_top'] = [
'#type' => 'item',
'#markup' => $text_top,
];
if ($count > 1) {
$form['item_id'] = [
'#type' => 'value',
'#value' => $this->itemId,
];
$form['order_id'] = [
'#type' => 'value',
'#value' => $this->orderId,
];
$form['item_id'] = [
'#title' => $this->t('Switch item'),
'#type' => 'select',
'#options' => $this->itemSelect,
'#default_value' => $this->itemId,
'#ajax' => [
'callback' => [$this, 'switchItem'],
'wrapper' => 'switch-item',
'method' => 'replace',
],
];
}
$this->gc = $this->gcs[$this->itemId];
$result = $this->partner->api([
'endpoint' => 'mandates',
'action' => 'get',
'mandate' => $this->gc->gc_mandate_id,
]);
if (!$result || !$result->response || $result->response->status_code != 200) {
$this->messenger()->addWarning($this->t('No active mandate found for this order at GoCardless.'));
return;
}
else {
$this->mandate = $result->response->body->mandates;
}
if ($this->item) {
$total = $this->item->getTotalPrice()->getNumber();
$data = $this->item->getData('gc');
$this->defaultAmount = number_format((float) $total, 2, '.', '');
$interval = isset($data['interval_params']) ? $data['interval_params']['string'] : NULL;
}
else {
$interval = NULL;
$data = [];
}
if (isset($data['shipment'])) {
foreach ($order->getAdjustments() as $adj) {
if ($adj->getType() == 'shipping') {
$adj_amount = $adj->getAmount()->getNumber();
$shipment_currency_code = $adj->getAmount()->getCurrencyCode();
$shipment_amount = round($adj_amount * $data['shipment']['proportion'], 2);
$this->defaultAmount += $shipment_amount;
$shipment_arr = [
'Shipping' => $this->currencyFormatter->format($shipment_amount, $shipment_currency_code),
];
break;
}
}
}
if ($is_recurring == 'child' && isset($order_gc['gc_payment_id'])) {
$result = $this->partner->api([
'endpoint' => 'payments',
'action' => 'get',
'id' => $order_gc['gc_payment_id'],
]);
if ($result && $result->response->status_code == 200) {
$payments = [$result->response->body->payments];
}
}
else {
$result = $this->partner->api([
'endpoint' => 'payments',
'action' => 'list',
'mandate' => $this->gc->gc_mandate_id,
'limit' => 500,
]);
if ($result && $result->response->status_code == 200) {
$payments = $result->response->body->payments;
}
}
// Data for one-off payments.
if ($this->gc->type == 'P') {
$item_data = [
'Mandate ID' => Html::escape($this->mandate->id),
'Mandate status' => Html::escape(ucfirst($this->mandate->status)),
'Created' => $this->dateFormatter->format(strtotime($this->mandate->created_at), 'short'),
'Type' => $this->t('Payments'),
'Amount' => isset($total) ? $this->currencyFormatter->format($total, $this->commerceCurrencyCode) : t('Unknown'),
];
if (isset($shipment_arr)) {
$item_data += $shipment_arr;
}
$item_data += [
'Interval' => $interval == ' ' ? '-' : $interval,
'Next possible charge date' => !is_null($this->mandate->next_possible_charge_date) ? $this->dateFormatter->format(strtotime($this->mandate->next_possible_charge_date), 'short') : '-',
'Next scheduled payment creation' => $this->mandate->status != 'cancelled' && !is_null($this->gc->next_payment) ? $this->dateFormatter->format($this->gc->next_payment, 'short') : '-',
];
if (array_key_exists('gc_end_type', $data)) {
if ($data['gc_end_type'] == 'count') {
$remaining = $data['gc_count'];
if (array_key_exists('gc_count_remaining', $data)) {
$remaining = $data['gc_count_remaining'];
}
$count = t('@remaining of @count payments remaining', [
'@remaining' => $remaining,
'@count' => $data['gc_count'],
]);
$item_data += ['Count' => $count];
}
if ($data['gc_end_type'] == 'fixed' || $data['gc_end_type'] == 'relative') {
$final_payment = t('Before the end of @end_time', [
'@end_time' => $this->dateFormatter->format(strtotime($data['gc_end_date']), 'short'),
]);
$item_data += ['Final payment creation' => $final_payment];
}
}
$this->defaultName = !$this->item ?: $this->itemSelect[$this->itemId];
}
// Data for subscriptions table.
elseif ($this->gc->type == 'S') {
$this->subscription = [];
if ($this->gc->gc_subscription_id) {
$result = $this->partner->api([
'endpoint' => 'subscriptions',
'action' => 'get',
'id' => $this->gc->gc_subscription_id,
]);
if ($result->response->status_code == 200) {
$this->subscription = $result->response->body->subscriptions;
}
}
if (!is_object($this->subscription) || !$this->gc->gc_subscription_id) {
$form['no_subscription'] =[
'#markup' => t('<div class="messages messages--warning">No subscription found at GoCardless for this line item.</div>'),
'#weight' => 0,
];
return $form;
}
$item_data = [
'Mandate ID' => Html::escape($this->mandate->id),
'Mandate status' => Html::escape(ucfirst($this->mandate->status)),
'Created' => $this->dateFormatter->format(strtotime($this->mandate->created_at), 'gocardless_client'),
'Type' => $this->t('Subscription'),
'Subscription status' => Html::escape(ucfirst($this->subscription->status)),
'Subscription name' => Html::escape($this->subscription->name),
'Subscription ID' => Html::escape($this->subscription->id),
'Subscription start' => $this->dateFormatter->format(strtotime($this->subscription->start_date), 'gocardless_client'),
'Amount' => $this->currencyFormatter->format($total, $this->commerceCurrencyCode),
];
if (isset($shipment_arr)) {
$item_data += $shipment_arr;
}
$item_data += [
'Interval' => $interval,
'Day of month' => Html::escape($this->subscription->day_of_month),
'Month' => ucfirst(Html::escape($this->subscription->month)),
'End date' => !is_null($this->subscription->end_date) ? $this->dateFormatter->format(strtotime($this->subscription->end_date), 'gocardless_client') : NULL,
];
// Assemble data for upcoming payments table.
$upcoming_rows = [];
foreach ($this->subscription->upcoming_payments as $upcoming) {
$upcoming_rows[] = [
$this->dateFormatter->format(strtotime($upcoming->charge_date), 'gocardless_client'),
$this->currencyFormatter->format($upcoming->amount / 100, $this->currencyCode),
];
}
$this->defaultName = Html::escape($this->subscription->name);
}
$item_rows = [];
foreach ($item_data as $item_data_key => $item_data_data) {
if (is_null($item_data_data)) {
continue;
}
$item_rows[] = [
$this->t('@key', ['@key' => $item_data_key]),
$item_data_data,
];
if ($is_recurring == 'child' && $item_data_key == 'Type') {
break;
}
}
$payment_rows = [];
$payment_total = 0;
$renderer = \Drupal::service('renderer');
if (isset($payments)) {
foreach ($payments as & $payment) {
if (isset($payment->links->subscription) && $payment->links->subscription != $this->gc->gc_subscription_id || isset($payment->metadata->item_id) && $payment->metadata->item_id != $this->itemId) {
continue;
}
// Assemble an Actions dropbutton.
$actions = ['#type' => 'dropbutton'];
if ($payment->status == 'pending_submission') {
$url = new Url(
'commerce_gc_client.payment_cancel_form', [
'commerce_order' => $this->orderId,
'payment_id' => $payment->id,
]);
$actions['#links']['cancel'] = [
'title' => $this->t('Cancel'),
'url' => $url,
];
}
if ($is_recurring == 'parent' && isset($order_gc['children'][$payment->id])) {
$url = new Url(
'commerce_gc_client.mandate', [
'commerce_order' => $order_gc['children'][$payment->id],
]);
$actions['#links']['child'] = [
'title' => $this->t('View recurring'),
'url' => $url,
];
}
elseif ($is_recurring == 'child' && isset($order_gc['parent'])) {
$url = new Url('commerce_gc_client.mandate',
['commerce_order' => $order_gc['parent']],
['query' => ['item_id' => $order_gc['item_id']]]
);
$actions['#links']['child'] = [
'title' => $this->t('View parent item'),
'url' => $url,
];
}
$payment_rows[$payment->id] = [
$this->dateFormatter->format(strtotime($payment->created_at), 'short'),
Html::escape($payment->description),
Html::escape($payment->id),
$this->currencyFormatter->format($payment->amount / 100, $this->currencyCode),
Html::escape($payment->status),
$this->dateFormatter->format(strtotime($payment->charge_date), 'gocardless_client'),
$renderer->render($actions),
];
if ($payment->status == 'confirmed' || $payment->status == 'paid_out') {
$payment_total += $payment->amount / 100;
}
}
}
$item_header = [[
'data' => 'Item data',
'colspan' => 2,
'style' => 'text-align:center;font-weight:bold;',
]];
$form['table'] = [
'#type' => 'table',
'#header' => $item_header,
'#rows' => isset($item_rows) ? $item_rows : NULL,
'#empty' => $this->t('There are no GoCardless items for this order'),
'#prefix' => '<div id="gc-item-table">',
'#suffix' => '</div>',
'#attached' => ['library' => ['commerce_gc_client/gocardless-client']],
];
if ($this->gc->type == 'S') {
$form['upcoming_payments'] = [
'#type' => 'details',
'#title' => $this->t('Upcoming payments'),
'#open' => FALSE,
'#prefix' => '<div id="gc-upcoming-payments-details">',
'#suffix' => '</div>',
];
$upcoming_header = [$this->t('Charge date'), $this->t('Amount')];
$form['upcoming_payments']['upcoming_payments_table'] = [
'#theme' => 'table',
'#header' => $upcoming_header,
'#rows' => isset($upcoming_rows) ? $upcoming_rows : NULL,
'#empty' => $this->t('There are no upcoming payments for this subscription.'),
];
}
if (isset($payment_rows)) {
$form['payment_tables'] = [
'#type' => 'details',
'#title' => $this->t('Payments'),
'#description' => $this->t('(Live data from GoCardless.com)'),
'#open' => TRUE,
];
$payment_header = [
$this->t('Created'),
$this->t('Description'),
$this->t('Payment ID'),
$this->t('Amount'),
$this->t('Status'),
$this->t('Charge customer at'),
'',
];
$empty = 'No payments have been created for the item yet.';
if ($this->gc->type == 'P') {
$empty .= ' Please check the "Payments" tab for details, if an Instant Payment was created during checkout.';
}
$form['payment_tables']['payments_table'] = [
'#theme' => 'table',
'#header' => $payment_header,
'#rows' => isset($payment_rows) ? $payment_rows : NULL,
'#empty' => $this->t($empty),
'#suffix' => $this->t('Total confirmed payments: @payment_total', [
'@payment_total' => $this->currencyFormatter->format($payment_total, $this->currencyCode),
]),
];
}
// If the order items have been removed or it is a recurring child order
// then we do not want the administration tools.
if (!$this->item || $is_recurring == 'child') {
return $form;
}
// Create payment subform.
if (in_array($this->mandate->status, [
'pending_submission',
'submitted',
'active',
])) {
$this->paymentCreateSubform($form, $form_state, $data, $currency_symbol);
}
if ($this->gc->type == 'S') {
if ($this->subscription->status != 'cancelled') {
// Update subscription section.
$this->subscriptionUpdateSubform($form, $form_state, $data, $currency_symbol);
// Cancel subscription button.
$subscription_cancel_message = $this->t('Are you sure you want to cancel this subscription?');
$form['cancel_subscription'] = [
'#type' => 'submit',
'#value' => $this->t('Cancel subscription at GoCardless'),
'#submit' => [[$this, 'subscriptionCancelSubmit']],
'#attributes' => [
'onclick' => 'if (!confirm("' . $subscription_cancel_message . '")) {return false;}',
],
];
}
}
if ($this->gc->type == 'P') {
if ($this->gc->gc_mandate_status !== 'cancelled') {
// Update payment creation section.
$this->paymentUpdateSubform($form, $form_state, $data, $commerce_currency_symbol, $total);
// Recurring orders section.
$this->recurringSubform($form, $form_state, $data);
}
// Schedule adjustments section.
$this->scheduledAdjustmentsSubform($form, $form_state, $currency_symbol);
}
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {}
/**
* Update Subscription subform.
*
* @param array $form
* The form.
* @param object $form_state
* The form_state object.
* @param array $data
* Array of module specific data for order item.
* @param string $currency_symbol
* The currency symbol relevant to the mandate.
*
* @return void
*/
private function subscriptionUpdateSubform(&$form, $form_state, $data, $currency_symbol) {
$form['update_subscription'] = [
'#type' => 'details',
'#title' => $this->t('Update subscription'),
'#open' => $this->currentRequest->query->get('action') == 'subscription_update',
];
$form['update_subscription']['subscription_name'] = [
'#type' => 'textfield',
'#title' => $this->t('Subscription name'),
'#default_value' => $this->defaultName,
];
$form['update_subscription']['subscription_amount'] = [
'#type' => 'number',
'#title' => $this->t('Amount') . ' ' . $currency_symbol,
'#size' => 10,
'#default_value' => $this->defaultAmount,
'#min' => 1,
'#step' => .01,
];
if (isset($data['shipment'])) {
$form['update_subscription']['shipment'] = [
'#type' => 'number',
'#title' => $this->t('Shipping proportion %'),
'#description' => $this->t("The proportion of the order's total shipping amount to allocate to this item."),
'#size' => 5,
'#default_value' => round($data['shipment']['proportion'] * 100, 2),
'#step' => .01,
'#min' => 0,
];
}
$form['update_subscription']['subscription_submit'] = [
'#type' => 'submit',
'#value' => $this->t('Update subscription details at GoCardless'),
'#submit' => [[$this, 'subscriptionUpdateSubmit']],
];
}
/**
* Subscription Update submit.
*/
public function subscriptionUpdateSubmit(array &$form, FormStateInterface $form_state) {
$shipment = $form_state->getValue('shipment');
if ($shipment || $shipment == 0) {
$gc = $this->item->getData('gc');
if ($gc['shipment']['proportion'] != (float) $shipment / 100) {
$gc['shipment']['proportion'] = (float) $shipment / 100;
$this->item->setData('gc', $gc);
$this->item->save();
$this->messenger()->addMessage($this->t('Shipping proportion has been updated for the item.'));
}
}
// Only update the subscription at GoCardless if the amount or the name
// have been changed, since this action can only be done a limited number
// of times.
$amount = $form_state->getValue('subscription_amount');
$name = $form_state->getValue('subscription_name');
if ($amount * 100 != $this->subscription->amount || $name != $this->defaultName) {
$result = $this->partner->api([
'endpoint' => 'subscriptions',
'action' => 'update',
'id' => $this->gc->gc_subscription_id,
'amount' => $form_state->getValue('subscription_amount') * 100,
'name' => $form_state->getValue('subscription_name'),
]
);
if ($result->response->status_code == 200) {
$this->messenger()->addMessage($this->t('Subscription was updated successfully at GoCardless.'));
}
else {
$this->messenger()->addWarning($this->t('There was a problem updating the subscription at GoCardless.'));
}
}
$form_state->setRedirect('commerce_gc_client.mandate', ['commerce_order' => $this->orderId], ['query' => [
'item_id' => $this->itemId,
'action' => 'subscription_update',
]]);
}
/**
* Subscription Cancellation submission.
*/
public function subscriptionCancelSubmit(array &$form, FormStateInterface $form_state) {
$result = $this->partner->api([
'endpoint' => 'subscriptions',
'action' => 'cancel',
'id' => $this->gc->gc_subscription_id,
]
);
if ($result->response->status_code == 200) {
$this->messenger()->addMessage($this->t('Subscription was cancelled successfully. Please review any pending payments and cancel if necessary.'));
}
else {
$this->messenger()->addWarning($this->t('There was a problem cancelling the subscription with GoCardless.'));
}
$form_state->setRedirect('commerce_gc_client.mandate', ['commerce_order' => $this->orderId], ['query' => [
'item_id' => $this->itemId,
]]);
}
/**
* Update Payment subform.
*
* @param array $form
* The form.
* @param object $form_state
* The form_state object.
* @param array $data
* Array of module specific data for order item.
* @param string $currency_symbol
* The currency symbol relevant to the mandate.
*
* @return void
*/
private function paymentCreateSubform(&$form, $form_state, $data, $currency_symbol) {
$form['create_payment'] = [
'#type' => 'details',
'#title' => $this->t('Create a payment'),
'#open' => $this->currentRequest->query->get('action') == 'payment_create',
];
if ($this->gc->type == 'S') {
$form['create_payment']['#description'] = $this->t('You can create one-off payments under the existing mandate for this item, which are in addition to the subscription plan listed here.');
}
$form['create_payment']['payment_amount'] = [
'#type' => 'number',
'#title' => $this->t('Amount') . ' ' . $currency_symbol,
'#size' => 10,
'#default_value' => $this->defaultAmount,
'#min' => 1,
'#step' => .01,
];
$form['create_payment']['payment_title'] = [
'#type' => 'textfield',
'#title' => $this->t('Payment title'),
'#default_value' => $this->t('Payment for @default_name', [
'@default_name' => $this->defaultName,
]),
];
$form['create_payment']['charge_date'] = [
'#title' => $this->t('Charge customer at'),
'#type' => 'date',
'#default_value' => date('Y-m-d', strtotime($this->mandate->next_possible_charge_date)),
];
$form['create_payment']['payment_submit'] = [
'#type' => 'submit',
'#value' => 'Instruct GoCardless to create a Payment',
'#validate' => [[$this, 'paymentCreateValidate']],
'#submit' => [[$this, 'paymentCreateSubmit']],
'#attributes' => [
'onclick' => 'if (!confirm("Are you sure you want to create a payment with GoCardless?")) {return false;}',
],
];
}
/**
* Create Payment form validation.
*/
public function paymentCreateValidate(array &$form, FormStateInterface $form_state) {
// Check that specified date is greater than or equal to the next possible
// charge date.
if (!empty($form_state->getValue('charge_date'))) {
if (strtotime($form_state->getValue('charge_date')) < strtotime($this->mandate->next_possible_charge_date)) {
$form_state->setErrorByName('charge_date', $this->t("The date cannot be before the 'Next Possible Charge Date'."));
}
}
}
/**
* Create Payment form submission.
*/
public function paymentCreateSubmit(array &$form, FormStateInterface $form_state) {
$amount = $form_state->getValue('payment_amount');
!empty($form_state->getValue('payment_title')) ? $title = $form_state->getValue('payment_title') : $title = $this->default_title;
$result = $this->partner->api([
'endpoint' => 'payments',
'action' => 'create',
'mandate' => $this->mandate->id,
'amount' => $amount,
'currency' => $this->currencyCode,
'description' => $title,
'charge_date' => $form_state->getValue('charge_date'),
'metadata' => ['item_id' => $this->itemId],
]);
if (!isset($result->response)) {
$this->messenger()->addWarning($this->t('There was a problem creating the payment with GoCardless'));
return;
}
elseif ($result->response->status_code == 201) {
$payment = $result->response->body->payments;
$this->messenger()->addMessage($this->t('Payment created successfully with GoCardless'));
}
$form_state->setRedirect('commerce_gc_client.mandate', ['commerce_order' => $this->orderId], ['query' => [
'item_id' => $this->itemId,
'action' => 'payment_create',
]]);
// Dispatch an event so that other modules can respond to payment creation.
if (isset($payment)) {
$event = new PaymentCreatedEvent($payment, $this->itemId, 'manual');
$this->eventDispatcher->dispatch($event, GoCardlessEvents::PAYMENT_CREATED);
}
}
/**
* Update Payment subform.
*
* @param array $form
* The form.
* @param object $form_state
* The form_state object.
* @param array $data
* Array of module specific data for order item.
*
* @return void
*/
private function paymentUpdateSubform(&$form, $form_state, $data, $commerce_currency_symbol, $total) {
$gc_end_type = 'none';
$count = null;
$remaining = null;
$end_date = null;
if (array_key_exists('gc_end_type', $data)) {
$gc_end_type = $data['gc_end_type'];
$count = $data['gc_count'];
$remaining = $data['gc_count'];
if (array_key_exists('gc_count_remaining', $data)) {
$remaining = $data['gc_count_remaining'];
}
$end_date = $data['gc_end_date'];
}
$form['update_payment'] = [
'#type' => 'details',
'#title' => $this->t('Update scheduled payments creation'),
'#open' => $this->currentRequest->query->get('action') == 'payment_update',
];
$form['update_payment']['next_payment'] = [
'#type' => 'datetime',
'#default_value' => $this->gc->next_payment ? DrupalDateTime::createFromTimestamp($this->gc->next_payment) : NULL,
'#date_year_range' => '0:+10',
'#description' => $this->t('Change the date that the next scheduled payment will be created. (This is not the same date that the customer will be charged on.)'),
];
$form['update_payment']['interval_length'] = [
'#type' => 'number',
'#title' => $this->t('Interval length'),
'#size' => 3,
'#maxlength' => 3,
'#default_value' => isset($data['interval_params']) ? $data['interval_params']['length'] : NULL,
'#min' => 1,
];
$form['update_payment']['interval_unit'] = [
'#type' => 'select',
'#title' => $this->t('Interval unit'),
'#default_value' => isset($data['interval_params']) ? $data['interval_params']['unit'] : NULL,
'#options' => [
'weekly' => $this->t('Week'),
'monthly' => $this->t('Month'),
'yearly' => $this->t('Year'),
],
'#empty_option' => $this->t('- select -'),
];
$form['update_payment']['amount'] = [
'#type' => 'number',
'#title' => $this->t('Amount') . ' ' . $commerce_currency_symbol,
'#size' => 5,
'#default_value' => number_format((float) $total, 2, '.', ''),
'#step' => .01,
'#min' => 0,
'#disabled' => TRUE,
];
if (isset($data['shipment'])) {
$form['update_payment']['shipment'] = [
'#type' => 'number',
'#title' => $this->t('Shipping proportion %'),
'#description' => $this->t("The proportion of the order's total shipping amount to allocate to this item."),
'#size' => 5,
'#default_value' => round($data['shipment']['proportion'] * 100, 2),
'#step' => .01,
'#min' => 0,
];
}
$form['update_payment']['final_payment'] = [
'#type' => 'details',
'#title' => $this->t('Final payment'),
'#open' => $gc_end_type == 'none' ? FALSE : TRUE,
];
$form['update_payment']['final_payment']['gc_end_type'] = [
'#type' => 'select',
'#title' => $this->t('Setting the final payment creation'),
'#default_value' => $gc_end_type,
'#options' => [
'none' => $this->t('There is no final payment'),
'count' => $this->t('After a specific number of payments'),
'fixed' => $this->t('Specific date'),
],
];
$form['update_payment']['final_payment']['gc_count'] = [
'#type' => 'number',
'#title' => $this->t('Count'),
'#size' => 5,
'#default_value' => $count,
'#min' => 1,
'#description' => t('The number of automatic payments that will be created from this point on.'),
'#states' => [
'visible' => [
':input[id="edit-gc-end-type"]' => [
'value' => 'count',
],
],
],
];
$form['update_payment']['final_payment']['gc_count_remaining'] = [
'#type' => 'number',
'#title' => $this->t('Remaining'),
'#size' => 5,
'#default_value' => $remaining,
'#min' => 0,
'#description' => t('The number of automatic payments remaining.'),
'#states' => [
'visible' => [
':input[id="edit-gc-end-type"]' => [
'value' => 'count',
],
],
],
];
$form['update_payment']['final_payment']['gc_end_date'] = [
'#type' => 'date',
'#default_value' => $end_date,
'#datepicker_options' => ['minDate' => 0],
'#description' => $this->t('Date on or after which no further payments will be created.'),
'#states' => [
'visible' => [
':input[id="edit-gc-end-type"]' => [
'value' => 'fixed',
],
],
'required' => [
':input[id="edit-gc-end-type"]' => [
'value' => 'fixed',
],
],
],
];
$form['update_payment']['update_payment_button'] = [
'#type' => 'submit',
'#value' => 'Update schedule',
'#validate' => [[$this, 'paymentUpdateValidate']],
'#submit' => [[$this, 'paymentUpdateSubmit']],
];
}
/**
* Update Payment form validation.
*/
public function paymentUpdateValidate(array &$form, FormStateInterface $form_state) {
$interval_length = $form_state->getValue('interval_length');
$interval_unit = $form_state->getValue('interval_unit');
if (empty($interval_length) && !empty($interval_unit)) {
$form_state->setErrorByName('interval_length', $this->t("An interval length must be selected as well as the interval unit."));
}
if (!empty($interval_length) && empty($interval_unit)) {
$form_state->setErrorByName('interval_unit', $this->t("An interval unit must be selected as well as the interval length."));
}
}
/**
* Update Payment form submission.
*/
public function paymentUpdateSubmit(array &$form, FormStateInterface $form_state) {
$values = $form_state->getValues();
$next_payment = $values['next_payment'] ? strtotime($values['next_payment']) : NULL;
$this->db->update('commerce_gc_client_item')
->fields([
'next_payment' => $next_payment,
])
->condition('item_id', $this->itemId)
->execute();
$interval_length = $values['interval_length'];
$interval_unit = $values['interval_unit'];
$gc = $this->item->getData('gc');
if ($interval_length) {
$gc['interval_params'] = [
'length' => $interval_length,
'unit' => $interval_unit,
'string' => $interval_length . ' ' . str_replace("ly", "", $interval_unit),
'gc' => $interval_length . ' ' . $interval_unit,
];
}
$shipment = $form_state->getValue('shipment');
if ($shipment || $shipment === 0) {
$gc['shipment']['proportion'] = (float) $shipment / 100;
}
if ($values['gc_end_type'] == 'count') {
$gc['gc_end_type'] = 'count';
$gc['gc_count'] = $values['gc_count'];
$gc['gc_count_remaining'] = $values['gc_count_remaining'];
}
elseif ($values['gc_end_type'] == 'fixed') {
$gc['gc_end_type'] = 'fixed';
$gc['gc_end_date'] = $values['gc_end_date'];
$gc['gc_end_time'] = strtotime('midnight +1 day', strtotime($values['gc_end_date']));
}
else {
$gc['gc_end_type'] = 'none';
}
$this->item->setData('gc', $gc);
$this->item->save();
$form_state->setRedirect('commerce_gc_client.mandate', ['commerce_order' => $this->orderId], ['query' => [
'item_id' => $this->itemId,
'action' => 'payment_update',
]]);
$this->messenger()->addMessage($this->t('Scheduled Payments Creation has been updated'));
}
/**
* Update Recurring Order subform.
*
* @param array $form
* The form.
* @param object $form_state
* The form_state object.
* @param array $data
* Array of module specific data for order item.
*
* @return void
*/
private function recurringSubform(&$form, $form_state, $data) {
$form['recurring'] = [
'#type' => 'details',
'#title' => $this->t('Update recurring order creation'),
'#open' => $this->currentRequest->query->get('action') == 'recurring_update',
];
$form['recurring']['create_order'] = [
'#type' => 'checkbox',
'#title' => $this->t("Create a recurring order upon creation of recurring payment?"),
'#default_value' => isset($data['gc_create_order']) ? $data['gc_create_order'] : FALSE,
];
$form['recurring']['email_invoice'] = [
'#type' => 'checkbox',
'#title' => $this->t("Email the user an invoice upon creation of a recurring order?"),
'#default_value' => isset($data['gc_email_invoice']) ? $data['gc_email_invoice'] : FALSE,
'#states' => [
'visible' => [
':input[id="edit-create-order"]' => [
'checked' => TRUE,
],
],
],
];
$form['recurring']['recurring_button'] = [
'#type' => 'submit',
'#value' => 'Update recurring order creation',
'#submit' => [[$this, 'recurringUpdateSubmit']],
];
}
/**
* Update Recurring Order form submission.
*/
public function recurringUpdateSubmit(array &$form, FormStateInterface $form_state) {
$gc = $this->item->getData('gc');
if ($create_order = $form_state->getValue('create_order')) {
$gc['gc_create_order'] = TRUE;
if ($email_invoice = $form_state->getValue('email_invoice')) {
$gc['gc_email_invoice'] = TRUE;
}
else {
$gc['gc_email_invoice'] = FALSE;
}
}
else {
$gc['gc_create_order'] = FALSE;
$gc['gc_email_invoice'] = FALSE;
}
$this->item->setData('gc', $gc);
$this->item->save();
$form_state->setRedirect('commerce_gc_client.mandate', ['commerce_order' => $this->orderId], ['query' => [
'item_id' => $this->itemId,
'action' => 'recurring_update',
]]);
$this->messenger()->addMessage($this->t('Recurring Orders Creation has been updated'));
}
/**
* Scheduled adjustments subform.
*
* @param array $form
* The form.
* @param object $form_state
* The form_state object.
* @param string $currency_symbol
* The relevant currency symbol for the mandate.
*
* @return void
*/
private function scheduledAdjustmentsSubform(&$form, $form_state, $currency_symbol) {
$query = $this->db->select('commerce_gc_client_item', 'i');
$query->join('commerce_gc_client_item_schedule', 's', 's.item_id=i.item_id');
$adjustments = $query->fields('s')->condition('i.item_id', $this->gc->item_id)->condition('s.type', 'adj')->orderBy('timestamp', 'ASC')->execute()->fetchAll();
if (!empty($adjustments)) {
$adj_headers = [
$this->t('Title'),
$this->t('Adjustment'),
$this->t('Date'),
$this->t('Status'),
'',
];
$adj_rows = [];
foreach ($adjustments as $adj) {
$actions = '';
if ($adj->status == 1) {
$actions = $this->t('<a href="@cancel_path">Cancel</a> @spacer', [
'@cancel_path' => '/gc_client/adjustment_action/cancel/' . $this->orderId . '/' . $adj->sid,
'@spacer' => '| ',
]);
}
$actions .= $this->t('<a href="@delete_path">Delete</a>', [
'@delete_path' => '/gc_client/adjustment_action/delete/' . $this->orderId . '/' . $adj->sid,
]);
if ($adj->status == 0) {
$status = $this->t('Cancelled');
}
elseif ($adj->status == 1) {
$status = $this->t('Pending');
}
elseif ($adj->status == 2) {
$status = $this->t('Complete');
}
$adj_data = unserialize($adj->data);
$adj_rows[] = [
$adj_data['title'],
$this->currencyFormatter->format($adj_data['amount'], $this->commerceCurrencyCode),
$adj->date,
$status,
new FormattableMarkup($actions, []),
];
}
}
$form['adjust'] = [
'#type' => 'details',
'#title' => $this->t('Schedule adjustments'),
'#description' => $this->t("Adjust the amount of scheduled payment creations for item."),
'#open' => $this->currentRequest->query->get('action') == 'adjustment_submit',
];
if ($this->gc->gc_mandate_status == 'cancelled' && !isset($adj_rows)) {
$form['adjust']['#access'] = FALSE;
}
if (isset($adj_rows)) {
$form['adjust']['adjust_table'] = [
'#type' => 'details',
'#title' => $this->t('Adjustments'),
'#open' => isset($_GET['adjustment']) ? TRUE : FALSE,
];
$form['adjust']['adjust_table']['table'] = [
'#type' => 'table',
'#header' => $adj_headers,
'#rows' => isset($adj_rows) ? $adj_rows : NULL,
];
}
if ($this->gc->gc_mandate_status != 'cancelled') {
$form['adjust']['adjust_title'] = [
'#type' => 'textfield',
'#size' => 24,
'#title' => $this->t('Adjustment title'),
'#default_value' => $this->t('Adjustment for @default_name', [
'@default_name' => $this->defaultName,
]),
];
$form['adjust']['adjustment'] = [
'#title' => $this->t('Adjustment amount') . ' ' . $currency_symbol,
'#size' => 10,
'#type' => 'number',
'#step' => .01,
'#default_value' => 0,
'#description' => $this->t('This will adjust the amount of a payment created with GoCardless.'),
];
$form['adjust']['payments'] = [
'#type' => 'number',
'#size' => 6,
'#title' => $this->t('Number of payments'),
'#default_value' => 1,
'#min' => 1,
];
$form['adjust']['starting_radio'] = [
'#type' => 'radios',
'#title' => $this->t('Starting from'),
'#options' => [
0 => $this->t('Next scheduled payment creation day'),
1 => $this->t('Select another date'),
],
'#default_value' => 0,
];
$form['adjust']['starting'] = [
'#type' => 'date',
'#title' => $this->t('Starting from'),
'#description' => $this->t('The adjustment will begin on the first scheduled billing date after that specified here.'),
'#states' => [
'visible' => [
':input[name="starting_radio"]' => [
'value' => 1,
],
],
],
];
$form['adjust']['plus'] = [
'#type' => 'details',
'#title' => $this->t('and then'),
'#open' => FALSE,
'#description' => $this->t("Additional scheduled adjustment(s) to follow initial adjustment(s)."),
];
$form['adjust']['plus']['plus_adjustment'] = [
'#title' => $this->t('Adjustment amount') . ' ' . $currency_symbol,
'#size' => 10,
'#type' => 'number',
'#step' => .01,
];
$form['adjust']['plus']['plus_payments'] = [
'#type' => 'number',
'#size' => 6,
'#title' => $this->t('Number of payments'),
'#default_value' => NULL,
'#min' => 1,
];
$form['adjust']['adjust_button'] = [
'#type' => 'submit',
'#value' => 'Schedule',
'#validate' => [[$this, 'adjustmentValidate']],
'#submit' => [[$this, 'adjustmentSubmit']],
];
}
}
/**
* Scheduled Adjustment validation.
*/
public function adjustmentValidate(array &$form, FormStateInterface $form_state) {
$adj = $form_state->getValue('adjustment');
$plus_adj = $form_state->getValue('plus_adjustment');
if ($adj == '' || $adj == '0') {
$form_state->setErrorByName('adjustment', $this->t("You must provide an adjustment value."));
}
if ($plus_adj == '0') {
$form_state->setErrorByName('plus_adjustment', $this->t("Adjustment value cannot be set to zero."));
}
if (empty($form_state->getValue('payments'))) {
$form_state->setValue('payments', 1);
}
if (!empty($plus_adj) && empty($form_state->getValue('plus_payments'))) {
$form_state->setValue('plus_payments', 1);
}
}
/**
* Scheduled Adjustment submission.
*/
public function adjustmentSubmit(array &$form, FormStateInterface $form_state) {
$ints = $this->item->getData('gc')['interval_params'];
$unit_price = $this->item->getUnitPrice()->getNumber();
$unit_currency_code = $this->item->getUnitPrice()->getCurrencyCode();
$query = $this->db->select('commerce_gc_client', 'g');
$query->join('commerce_gc_client_item', 'i', 'g.gcid=i.gcid');
$starting = $query->fields('i', ['next_payment'])->condition('item_id', $this->itemId)->execute()->fetchField();
if ($form_state->getValue('starting_radio') == '1') {
$select_date = strtotime($form_state->getValue('starting'));
while ($starting < $select_date) {
$string_ = '+' . $ints['string'];
$starting = strtotime($string_, $starting);
}
}
// Create array containing scheduled dates.
$inserts = [];
$payments = $form_state->getValue('payments');
$amount = $form_state->getValue('adjustment');
for ($i = 0; $i < $payments; $i++) {
$string_ = '+' . ($i * $ints['length']) . ' ' . str_replace('ly', '', $ints['unit']);
$inserts[] = [
'timestamp' => strtotime($string_, $starting),
'amount' => $amount,
];
$ending = strtotime($string_, $starting);
}
if (!empty($form_state->getValue('plus_adjustment'))) {
// Create array containing additional scheduled dates.
$plus_amount = $form_state->getValue('plus_adjustment');
$plus_starting = strtotime('+' . $ints['string'], $ending);
$plus_payments = $form_state->getvalue('plus_payments');
for ($i = 0; $i < $plus_payments; $i++) {
$string_ = '+' . ($i * $ints['length']) . ' ' . str_replace('ly', '', $ints['unit']);
$inserts[] = [
'timestamp' => strtotime($string_, $plus_starting),
'amount' => $plus_amount,
];
}
}
// Add schedules data to database.
foreach ($inserts as $insert) {
// Check validity of new scheduled adjustments and disallow if it will
// cause a payment creation of less than one and not zero.
$insert_date = date('d M Y', $insert['timestamp']);
// Calculate sum of scheduled adjs for date.
$query = $this->db->select('commerce_gc_client_item', 'i');
$query->join('commerce_gc_client_item_schedule', 's', 'i.item_id = s.item_id');
$scheds = $query->fields('s')
->condition('s.type', 'adj')
->condition('s.status', 1)
->condition('s.date', $insert_date)
->condition('i.item_id', $this->itemId)
->execute()
->fetchAll();
$sum = 0;
foreach ($scheds as $sched) {
$sched_data = unserialize($sched->data);
$sum = $sum + $sched_data['amount'];
}
$sum = ($sum + $insert['amount'] + $unit_price);
if ($sum < 1 && $sum != 0) {
$this->messenger()->addWarning($this->t('The schedule adjustment for @date cannot be placed because the price of the item inluding adjustments is not zero, and is less than @amount, which is not allowed by GoCardless.', [
'@date' => $insert_date,
'@amount' => $this->currencyFormatter->format(1, $unit_currency_code),
]));
continue;
}
$insert_date = date('d M Y', $insert['timestamp']);
$this->db->insert('commerce_gc_client_item_schedule')->fields([
'item_id' => $this->itemId,
'type' => 'adj',
'date' => $insert_date,
'timestamp' => strtotime($insert_date),
'status' => 1,
'data' => serialize([
'title' => !empty($form_state->getValue('adjust_title')) ? $form_state->getValue('adjust_title') : $this->t('Adjustment'),
'amount' => $insert['amount'],
]),
'created' => $this->time->getRequestTime(),
])->execute();
$this->messenger()->addMessage($this->t('Schedule adjustment created for @date.', [
'@date' => $insert_date,
]));
}
$form_state->setRedirect('commerce_gc_client.mandate', ['commerce_order' => $this->orderId], ['query' => [
'action' => 'adjustment_submit',
'item_id' => $this->itemId,
]]);
}
/**
* AJAX callback function.
*
* Reloads the form.
*/
public static function switchItem(array &$form, FormStateInterface $form_state) {
$item_id = $form_state->getValue('item_id');
$order_id = $form_state->getValue('order_id');
$path = '/admin/commerce/orders/' . $order_id . '/gocardless?item_id=' . $item_id;
$response = new AjaxResponse();
$response->addCommand(new RedirectCommand($path));
return $response;
}
}
