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