contacts_events-8.x-1.x-dev/src/Plugin/Commerce/CheckoutPane/BookingPaymentTrait.php
src/Plugin/Commerce/CheckoutPane/BookingPaymentTrait.php
<?php
namespace Drupal\contacts_events\Plugin\Commerce\CheckoutPane;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneInterface;
use Drupal\commerce_price\Price;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\contacts_events\Event\BookingCompletionValidationEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Trait for shared code between the booking payment panes.
*/
trait BookingPaymentTrait {
/**
* The order.
*
* @var \Drupal\commerce_order\Entity\OrderInterface
*/
protected $order;
/**
* The order item tracking service, if available.
*
* @var \Drupal\commerce_partial_payments\OrderItemTrackingInterface|null
*/
protected $paymentTracking;
/**
* The booking payment private tempstore.
*
* @var \Drupal\Core\TempStore\PrivateTempStore
*/
protected $tempstore;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* The element info manager.
*
* @var \Drupal\Core\Render\ElementInfoManagerInterface
*/
protected $elementInfoManager;
/**
* Initialise dependencies on the plugin.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The container.
* @param \Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneInterface $plugin
* The plugin.
*/
protected static function init(ContainerInterface $container, CheckoutPaneInterface $plugin) {
/** @var static $plugin */
$plugin->tempstore = $container->get('tempstore.private')->get('contacts_events.booking_flow');
$plugin->eventDispatcher = $container->get('event_dispatcher');
$plugin->elementInfoManager = $container->get('plugin.manager.element_info');
if ($container->has('commerce_partial_payments.order_item_tracking')) {
$plugin->paymentTracking = $container->get('commerce_partial_payments.order_item_tracking');
}
}
/**
* Get the total deposits required to confirm the booking.
*
* @param array|null $tracking
* An array to be filled with the payment tracking information.
*
* @return \Drupal\commerce_price\Price|null
* The deposit amount, or NULL if deposits are not available/there are no
* deposits to be paid.
*/
protected function getDepositTotal(?array &$tracking = []): ?Price {
$deposit_amount = $this->getDepositAmount();
if (!$deposit_amount) {
return NULL;
}
// If we have tracking, get the paid amount.
$paid = $this->paymentTracking ? $this->paymentTracking->getTrackedAmountsForOrder($this->order) : [];
/** @var \Drupal\commerce_price\Price|null $deposit_total */
$deposit_total = NULL;
foreach ($this->order->getItems() as $item) {
// If this item is not stateful, include it in the deposit. If we have
// tracking an it has already had the deposit paid, it will be reduced.
if (!$item->hasField('state') || $item->get('state')->value == 'pending') {
// The deposit is the lower of the event deposit amount and the item
// price.
$item_amount = $item->getAdjustedTotalPrice();
$item_deposit = $item_amount->lessThan($deposit_amount) ? $item_amount : $deposit_amount;
// If we have tracking, we should reduce the deposit by what has already
// been paid.
// @todo Non stateful items will be required as a deposit even if a
// deposit has already been paid unless we have tracking.
if (isset($paid[$item->id()])) {
$item_deposit = $item_deposit->subtract($paid[$item->id()]);
}
// Only consider positive deposit amounts.
if ($item_deposit->isPositive()) {
// Set up tracking as per commerce partial payments.
$tracking[] = ['target_id' => $item->id()] + $item_deposit->toArray();
$deposit_total = $deposit_total ? $deposit_total->add($item_deposit) : $item_deposit;
}
}
}
return $deposit_total;
}
/**
* Get the payment amount from the private tempstore.
*
* @return \Drupal\commerce_price\Price|null
* The payment amount, or NULL if there is none specified.
*/
protected function getPaymentAmount(): ?Price {
$amount = $this->tempstore->get("{$this->order->id()}:payment_amount");
return $amount ? Price::fromArray($amount) : NULL;
}
/**
* Set the payment amount. This is stored in the private tempstore.
*
* @param \Drupal\commerce_price\Price|null $amount
* The payment amount, or NULL to clear it.
*/
protected function setPaymentAmount(?Price $amount): void {
if ($amount) {
$this->tempstore->set("{$this->order->id()}:payment_amount", $amount->toArray());
}
else {
$this->tempstore->delete("{$this->order->id()}:payment_amount");
}
}
/**
* Build the payment intro.
*
* @param array $complete_form
* The complete form.
*
* @return array
* The payment intro.
*/
protected function buildPaymentIntro(array $complete_form): array {
$intro['title'] = [
'#type' => 'html_tag',
'#tag' => 'h2',
'#value' => $this->t('Payment'),
];
$intro['text'] = [
'#type' => 'html_tag',
'#tag' => 'p',
];
// Adjust the text based on the next action.
if (isset($complete_form['actions']['next']) && $complete_form['actions']['next']['#type'] == 'submit') {
$intro['text']['#value'] = $this->t('To complete your booking, please check the details and click the %label button.', [
'%label' => $complete_form['actions']['next']['#value'],
]);
}
else {
$intro['text']['#value'] = $this->t('Your booking is confirmed and there is nothing to pay.');
}
return $intro;
}
/**
* Add a warning if there are pending payments.
*
* @param array $pane_form
* The pane form.
*/
protected function addPendingWarning(array &$pane_form): void {
if ($pane_form['balances']['#has_pending']) {
$pane_form['pending_warning'] = [
'#theme' => 'status_messages',
'#message_list' => [
'warning' => [$this->t('You have pending payments. Continuing may result in over payment.')],
],
];
}
}
/**
* Run the completion validation, preventing continuation on error.
*
* @param array $complete_form
* The complete form array.
*
* @return bool
* TRUE if there are no errors, FALSE if there are. In either case there may
* be warnings.
*/
protected function runCompletionValidation(array &$complete_form): bool {
// Validate that we are OK to proceed with completing the checkout process.
$validation_event = new BookingCompletionValidationEvent($this->order, $this->currentUser->hasPermission('can manage bookings for contacts_events'));
$this->eventDispatcher->dispatch(BookingCompletionValidationEvent::NAME, $validation_event);
// Add warnings and messages.
foreach ($validation_event->getWarnings() as $message) {
$this->messenger()->addWarning($message);
}
foreach ($validation_event->getErrors() as $message) {
$this->messenger()->addError($message);
}
// Completely stop proceeding if we have an error.
if ($validation_event->hasErrors()) {
$complete_form['actions']['next']['#access'] = FALSE;
return FALSE;
}
return TRUE;
}
/**
* Ge the payment summary information.
*
* @return array
* The render array.
*/
public function getSummary() {
$build = [
'#type' => 'html_tag',
'#tag' => 'dl',
'#has_pending' => FALSE,
'#pending_paid_in_full' => FALSE,
'total' => [
'#weight' => 0,
'title' => $this->buildSummaryTitle($this->t('Booking total')),
'value' => $this->buildSummaryAmount($this->order->getTotalPrice()),
],
'paid' => [
'#weight' => 50,
'title' => $this->buildSummaryTitle($this->t('Paid so far')),
'value' => $this->buildSummaryAmount($this->order->getTotalPaid()),
],
'balance' => [
'#weight' => 90,
'title' => $this->buildSummaryTitle($this->t('Outstanding balance')),
'value' => $this->buildSummaryAmount($this->order->getBalance()),
],
];
// Show pending payments so users don't get confused.
$pending = $this->checkoutFlow->getPendingAmounts();
if ($pending) {
$build['#has_pending'] = TRUE;
$build['#pending_paid_in_full'] = FALSE;
$build['pending']['#weight'] = 99;
$balance = $this->order->getBalance();
// Load the gateways for their labels.
$gateways = $this->entityTypeManager
->getStorage('commerce_payment_gateway')
->loadMultiple(array_keys($pending));
foreach ($pending as $gateway_id => $amount) {
$build['pending'][$gateway_id]['title'] = $this->buildSummaryTitle($this->t('Pending @label', ['@label' => $gateways[$gateway_id]->label()]));
/** @var \Drupal\commerce_price\Price $pending_amount */
foreach ($amount as $pending_amount) {
if ($balance->getCurrencyCode() === $pending_amount->getCurrencyCode()) {
$balance = $balance->subtract($pending_amount);
}
$build['pending'][$gateway_id]['value'][$pending_amount->getCurrencyCode()] = $this->buildSummaryAmount($pending_amount);
}
}
if (!$balance->isPositive()) {
$build['#pending_paid_in_full'] = TRUE;
}
}
return $build;
}
/**
* Build the dt for a summary item title.
*
* @param string|\Drupal\Component\Render\MarkupInterface|null $title
* The title of the item.
*
* @return array
* The render array for the dt.
*/
protected function buildSummaryTitle($title): array {
return [
'#type' => 'html_tag',
'#tag' => 'dt',
'#value' => $title,
];
}
/**
* Build the dd for a summary item amount.
*
* @param \Drupal\commerce_price\Price|null $amount
* The amount to render. If NULL, it will render a zero amount in the
* relevant currency.
* @param string|\Drupal\Component\Render\MarkupInterface|null $prefix
* An optional prefix for the amount.
* @param string|\Drupal\Component\Render\MarkupInterface|null $suffix
* An optional suffix for the amount.
*
* @return array
* The render array for the dd.
*/
protected function buildSummaryAmount(?Price $amount, $prefix = NULL, $suffix = NULL) {
// Get a 'zero' if the amount is NULL.
if (!$amount) {
$order_total = $this->order->getTotalPrice();
$default_currency = $order_total ?
$order_total->getCurrencyCode() :
$this->order->getStore()->getDefaultCurrencyCode();
$amount = new Price(0, $default_currency);
}
// Use a static to avoid having to compute this multiple times.
static $pre_render;
if (!isset($pre_render)) {
$pre_render = $this->elementInfoManager
->getInfoProperty('html_tag', '#pre_render', []);
array_unshift($pre_render, [$this, 'preRenderAmount']);
}
// Build the render array.
return [
'#type' => 'html_tag',
'#tag' => 'dd',
'#amount' => $amount,
'#amount_prefix' => $prefix,
'#amount_suffix' => $suffix,
'#pre_render' => $pre_render,
];
}
/**
* Pre render callback for format an amount.
*
* @param array $element
* The element with #amount.
*
* @return array
* The element with #markup set.
*/
public function preRenderAmount(array $element) {
if (!isset($element['#value']) && isset($element['#amount'])) {
$element['#value'] = new FormattableMarkup('@prefix@amount@suffix', [
'@amount' => $this->formatPrice($element['#amount']),
'@prefix' => $element['#amount_prefix'] ?? '',
'@suffix' => $element['#amount_suffix'] ?? '',
]);
}
return $element;
}
}
