contacts_events-8.x-1.x-dev/src/Plugin/Commerce/CheckoutPane/BookingPaymentInformation.php
src/Plugin/Commerce/CheckoutPane/BookingPaymentInformation.php
<?php namespace Drupal\contacts_events\Plugin\Commerce\CheckoutPane; use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface; use Drupal\commerce_partial_payments\Element\TrackedAmounts; use Drupal\commerce_payment\Plugin\Commerce\CheckoutPane\PaymentInformation; use Drupal\commerce_price\Price; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Security\TrustedCallbackInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides the review pane. * * @CommerceCheckoutPane( * id = "booking_payment_information", * label = @Translation("Booking payment information"), * default_step = "review", * ) */ class BookingPaymentInformation extends PaymentInformation implements TrustedCallbackInterface { use BookingPaymentTrait; /** * The currency formatter. * * @var \CommerceGuys\Intl\Formatter\CurrencyFormatterInterface */ protected $currencyFormatter; /** * The deposit amount, NULL if not enabled and FALSE if not checked. * * @var \Drupal\commerce_price\Price|null|false */ protected $depositAmount = FALSE; /** * The stripe intent order subscriber, if any. * * @var \Drupal\contacts_events\EventSubscriber\StripeIntentOrderSubscriber|null */ protected $stripeIntentSubscriber; /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow = NULL) { /** @var static $plugin */ $plugin = parent::create($container, $configuration, $plugin_id, $plugin_definition, $checkout_flow); $plugin->currencyFormatter = $container->get('commerce_price.currency_formatter'); $plugin->stripeIntentSubscriber = $container->get('commerce_stripe.order_events_subscriber', ContainerInterface::NULL_ON_INVALID_REFERENCE); static::init($container, $plugin); return $plugin; } /** * {@inheritdoc} */ public function getDisplayLabel() { return NULL; } /** * {@inheritdoc} */ public function buildPaneSummary() { return $this->getSummary(); } /** * {@inheritdoc} */ public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) { $pane_form['intro'] = $this->buildPaymentIntro($complete_form); $pane_form['balances'] = $this->getSummary(); $this->addPendingWarning($pane_form); $this->runCompletionValidation($complete_form); $this->buildPaymentOptions($pane_form); $pane_form = parent::buildPaneForm($pane_form, $form_state, $complete_form); return $pane_form; } /** * {@inheritdoc} */ public function validatePaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) { parent::validatePaneForm($pane_form, $form_state, $complete_form); // Prevent validation errors on the custom selection if we aren't proceeding // with custom payment amounts. if (isset($pane_form['amount_option_custom']['#parents']) && $this->getAmountOption($pane_form, $form_state) != 'custom') { $name_prefix = implode('][', $pane_form['amount_option_custom']['#parents']); $errors = $form_state->getErrors(); $form_state->clearErrors(); foreach ($errors as $name => $error) { if (strpos($name, $name_prefix) !== 0) { $form_state->setErrorByName($name, $error); } } } } /** * {@inheritdoc} */ public function submitPaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) { parent::submitPaneForm($pane_form, $form_state, $complete_form); /** @var \Drupal\commerce_price\Price|null $amount */ $amount = NULL; switch ($this->getAmountOption($pane_form, $form_state)) { // For deposits, get the amount and, if partial payments is available, // build the tracking information. case 'deposits': $amount = $this->getDepositTotal($tracking_values); break; // For custom (choose per ticket) we need to store the tracking // information. case 'custom': $tracking_values = $form_state->getValue($pane_form['amount_option_custom']['#parents']); $tracking_values = TrackedAmounts::extractValues($tracking_values, $amount); break; } // Pass the payment amount onto the process step if it's not the full // balance. $this->setPaymentAmount($amount); // If we have payment tracking, store the information. if ($this->paymentTracking && !empty($tracking_values)) { $this->paymentTracking->storeTrackingInformation($this->order, $tracking_values); } // If we have the stripe subscriber, make sure the intent is marked for // update. if ($this->stripeIntentSubscriber) { $this->stripeIntentSubscriber->updateOrder($this->order); } } /** * Get the amount option from the form/form state. * * @param array $pane_form * The pane form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state. * * @return string * The payment amount option. */ protected function getAmountOption(array $pane_form, FormStateInterface $form_state) { if (count($pane_form['amount_option']['#options']) == 1) { return array_keys($pane_form['amount_option']['#options'])[0]; } else { return $form_state->getValue($pane_form['amount_option']['#parents']); } } /** * Get the deposit amount for the event. * * @return \Drupal\commerce_price\Price|null * The deposit amount for the event, or NULL if deposits are not enabled. */ protected function getDepositAmount(): ?Price { if ($this->depositAmount === FALSE) { $this->depositAmount = $this->getSettingAmount('payment.deposit'); } return $this->depositAmount; } /** * Get a price amount from an event setting. * * @param string $setting_key * The event setting key. * * @return \Drupal\commerce_price\Price|null * A price or NULL if not configured. */ protected function getSettingAmount(string $setting_key): ?Price { /** @var \Drupal\contacts_events\Entity\EventInterface|null $event */ $event = $this->order->get('event')->entity; if (!$event) { return NULL; } $amount = $event->getSetting($setting_key); return $amount ? Price::fromArray($amount) : NULL; } /** * Process callback for the amount options radios to hide if only one choice. * * @param array $element * The element array. * * @return array * The element array. */ public static function hideSingleOption(array $element) { if (count($element['#options']) == 1) { $element['#access'] = FALSE; $element['#required'] = FALSE; $element['#value'] = array_keys($element['#options'])[0]; } return $element; } /** * {@inheritdoc} */ protected function buildBillingProfileForm(array $pane_form, FormStateInterface $form_state) { // We already have the billing details from a previous step, but the submit // handler requires the billing profile in the form array. $pane_form = parent::buildBillingProfileForm($pane_form, $form_state); $pane_form['billing_information']['#access'] = FALSE; return $pane_form; } /** * Format a price object. * * @param \Drupal\commerce_price\Price $price * The price. * * @return string * The formatted price. */ protected function formatPrice(Price $price) { return $this->currencyFormatter->format($price->getNumber(), $price->getCurrencyCode()); } /** * Build the payment options selection. * * @param array $pane_form * The pane form. */ protected function buildPaymentOptions(array &$pane_form): void { // Payment options are only applicable if there is a balance. $balance = $this->order->getBalance(); if (!$balance || !$balance->isPositive()) { return; } $pane_form['amount_option'] = [ '#type' => 'radios', '#title' => $this->t('Payment options'), '#required' => TRUE, '#process' => $this->elementInfoManager->getInfoProperty('radios', '#process', []), '#options' => [ 'full' => $this->t('Pay in full %amount', [ '%amount' => $this->formatPrice($balance), ]), ], 'full' => [ '#description' => $this->t('Pay all outstanding balances and secure your price now.'), ], ]; // phpcs:ignore Drupal.Arrays.Array.LongLineDeclaration $pane_form['amount_option']['#process'][] = [static::class, 'hideSingleOption']; /** @var \Drupal\contacts_events\Entity\EventInterface $event */ $event = $this->order->get('event')->entity; // Show the deposit option, if enabled. $deposit_amount = $this->getDepositAmount(); if ($deposit_total = $this->getDepositTotal()) { $pane_form['amount_option']['#options']['deposits'] = $this->t('Pay all deposits %amount', [ '%amount' => $this->formatPrice($deposit_total), ]); $pane_form['amount_option']['deposits']['#weight'] = -50; $pane_form['amount_option']['deposits']['#description'] = $this->t('You can reserve your tickets by paying a deposit of @amount per ticket. The price will go up if not paid in full by the next booking deadline.', [ '@amount' => $this->formatPrice($deposit_amount), ]); } // Show the installments option, if enabled. if ($this->paymentTracking && $event->getSetting('payment.instalments')) { $pane_form['amount_option']['#options']['custom'] = $this->t('Choose per ticket'); $pane_form['amount_option']['custom']['#description'] = $this->t('Pay an instalment on any or all of your tickets.'); $tracking = $this->paymentTracking->getTrackedAmountsForOrder($this->order); $full_tracking = $this->paymentTracking->calculatePaidInFullTracking($this->order); // Override the pay in full option amount if the default tracking is not // the same as the order balance. /** @var \Drupal\commerce_price\Price|null $tracking_total */ $tracking_total = NULL; foreach ($full_tracking as $tracking_item) { $item_amount = Price::fromArray($tracking_item); $tracking_total = $tracking_total ? $tracking_total->add($item_amount) : $item_amount; } if (!$tracking_total->equals($balance)) { $pane_form['amount_option']['#options']['full'] = $this->t('Pay in full %amount', [ '%amount' => $this->formatPrice($tracking_total), ]); } // Build our custom amounts option. $pane_form['amount_option_custom'] = [ '#type' => 'commerce_partial_payments_tracked_amount', '#tracking' => $tracking, '#default_value' => $full_tracking, '#items' => [], '#header' => [ 'label' => '', 'status' => $this->t('Status'), 'price' => $this->t('Price'), 'balance' => $this->t('Outstanding'), 'number' => $this->t('Payment'), ], '#states' => [ 'visible' => [ ':input[name="booking_payment_information[amount_option]"]' => ['value' => 'custom'], ], ], ]; foreach ($this->order->getItems() as $item) { $item_id = $item->id(); $price = $item->getAdjustedTotalPrice(); $is_ticket = $item->bundle() == 'contacts_ticket'; // Don't show non-ticket items that have no value. if (!$is_ticket && (!$price || $price->isZero())) { continue; } /** @var \Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface|null $item_state */ $item_state = $item->hasField('state') ? $item->get('state')->first() : NULL; $balance = $price ? $price->subtract($tracking[$item_id]) : NULL; /** @var \Drupal\commerce_price\Price|null $balance */ $has_balance = $balance && $balance->isPositive(); $pane_form['amount_option_custom']['#items'][$item_id] = [ 'label' => $item->label(), 'status' => $item_state ? $item_state->getLabel() : NULL, 'price' => $item->getAdjustedTotalPrice(), 'number:hide' => !$has_balance, ]; if ($has_balance) { // If the item is not stateful or confirmed, we need to set a minimum // amount. if (!$item_state || $item_state->value == 'pending') { // Non stateful items should always be paid in full. if (!$item_state) { $min_amount = $balance->getNumber(); } // Otherwise, if we have deposits, take the minimum of the deposit // and the price. elseif ($deposit_amount) { if ($deposit_amount->lessThan($price)) { $min_amount = $deposit_amount->getNumber(); } else { $min_amount = $price->getNumber(); } } // Otherwise, just make sure it's non-zero. else { // Price has a scale of 6, so this enforces non-zero across all // currency. $min_amount = 0.000001; } $pane_form['amount_option_custom']['#items'][$item_id]['number:min'] = $min_amount; } $pane_form['amount_option_custom']['#items'][$item_id]['number:max'] = $balance->getNumber(); } } } } /** * {@inheritdoc} */ public static function trustedCallbacks() { return ['preRenderAmount']; } }