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