contacts_events-8.x-1.x-dev/src/Plugin/Commerce/CheckoutFlow/BookingFlow.php
src/Plugin/Commerce/CheckoutFlow/BookingFlow.php
<?php
namespace Drupal\contacts_events\Plugin\Commerce\CheckoutFlow;
use Drupal\commerce\Response\NeedsRedirectException;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowWithPanesBase;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\HasPaymentInstructionsInterface;
use Drupal\commerce_price\Price;
use Drupal\contacts_events\OrderStateTrait;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Url;
/**
* Provides the default multistep checkout flow.
*
* @CommerceCheckoutFlow(
* id = "booking_flow",
* label = "Booking Flow",
* )
*/
class BookingFlow extends CheckoutFlowWithPanesBase implements BookingFlowInterface {
use OrderStateTrait;
const ROUTE_NAME = 'entity.commerce_order.booking_process';
/**
* {@inheritdoc}
*/
public function setOrder(OrderInterface $order) {
$this->order = $order;
return $this;
}
/**
* Gets the order.
*
* This is invoked by
* Drupal\commerce_order\Form\OrderItemInlineForm::buildOrderItem.
*/
public function getEntity() {
return $this->order;
}
/**
* {@inheritdoc}
*/
public function getSteps() {
return [
'summary' => [
'label' => $this->t('Dashboard'),
'has_sidebar' => FALSE,
],
'tickets' => [
'label' => $this->t('Tickets'),
'previous_label' => $this->t('Back to tickets'),
'next_label' => $this->t('Continue to add tickets'),
'has_sidebar' => FALSE,
],
'accommodation' => [
'label' => $this->t('Accommodation'),
'previous_label' => $this->t('Back to accommodation'),
'next_label' => $this->t('Continue to choose your accommodation'),
'has_sidebar' => FALSE,
],
'address' => [
'label' => $this->t('Delivery & Billing Address'),
'previous_label' => $this->t('Back to your details'),
'next_label' => $this->t('Continue to your details'),
'has_sidebar' => FALSE,
],
'terms' => [
'label' => $this->t('Terms and Conditions'),
'next_label' => $this->t('Review and pay'),
'has_sidebar' => FALSE,
'hidden' => TRUE,
],
'review' => [
'display_label' => $this->t('Payment'),
'label' => $this->t('Payment (Review)'),
'next_label' => $this->t('Review and pay'),
'has_sidebar' => FALSE,
],
'payment' => [
'display_label' => $this->t('Payment'),
'label' => $this->t('Payment (Process)'),
'next_label' => $this->t('Make payment'),
'has_sidebar' => FALSE,
'hidden' => TRUE,
],
'complete' => [
'label' => $this->t('Complete'),
'has_sidebar' => FALSE,
'hidden' => TRUE,
],
];
}
/**
* {@inheritdoc}
*/
public function getInitialStep(): string {
$steps = array_keys($this->getVisibleSteps());
$step = array_shift($steps);
if ($step == 'summary') {
$step = array_shift($steps);
}
return $step;
}
/**
* {@inheritdoc}
*/
public function getContinueUrl(): Url {
$url = $this->order->toUrl('booking_process');
if ($this->order->getState()->value == 'draft') {
$url->setRouteParameter('step', $this->order->get('checkout_step')->value);
}
return $url;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $step_id = NULL) {
// If complete, redirect to the dashboard with a message.
if ($step_id == 'complete') {
$this->messenger()->addStatus($this->t('Thank you for confirming your booking. You will receive an email confirmation shortly with the details of your booking.'));
$payment_order_updater = \Drupal::service('commerce_payment.order_updater');
$payment_order_updater->requestUpdate($this->order);
// If there are any outstanding payments, show any relevant payment
// instructions. This is necessary as we don't show the normal completion
// page.
$this->showPaymentInstructions($this->order);
$this->redirectToStep('summary');
}
return parent::buildForm($form, $form_state, $step_id);
}
/**
* {@inheritdoc}
*/
public function getNextStepId($step_id) {
// Redirect to the summary if in doubt.
return parent::getNextStepId($step_id) ?? 'summary';
}
/**
* {@inheritdoc}
*/
public function redirectToStep($step_id) {
// Only update the order if we are progressing to a later state.
$this->order->set('checkout_step', $step_id);
$this->onStepChange($step_id);
$this->order->save();
// Use our route rather than the default commerce_checkout.form.
throw new NeedsRedirectException(Url::fromRoute(self::ROUTE_NAME, [
'commerce_order' => $this->order->id(),
'step' => $step_id,
])->toString());
}
/**
* {@inheritdoc}
*/
protected function onStepChange($step_id) {
// Prevent changing step to a previous step or storing the complete step.
$current_step = $this->entityTypeManager
->getStorage('commerce_order')
->loadUnchanged($this->order->id())
->get('checkout_step')->value;
// Get the order of the steps.
$steps = array_keys($this->getSteps());
$current_pos = array_search($current_step, $steps);
$new_pos = array_search($step_id, $steps);
$payment_pos = array_search('payment', $steps);
if ($current_step == 'complete' || $new_pos < $current_pos) {
// If the old position was earlier than the payment step, preserve it.
// Otherwise set it to review.
$this->order->set('checkout_step', $current_pos < $payment_pos ? $current_step : 'review');
}
// Lock the order while on the 'payment' checkout step. Unlock elsewhere.
if ($step_id == 'payment') {
$this->order->lock();
}
elseif ($step_id != 'payment') {
$this->order->unlock();
}
// Place the order or move to confirmed paid in full.
if ($step_id == 'complete') {
$transition_id = 'place';
$balance = $this->order->getBalance();
// If balance is non-positive trigger both place and fully paid events.
if (!$balance || $balance->isZero() || $balance->isNegative()) {
$transition_id = 'confirmed_paid_in_full';
}
$state = $this->order->getState();
$this->applyTransitionIfAllowed($state, $transition_id);
}
}
/**
* {@inheritdoc}
*/
public function needsPayment(bool $include_pending = FALSE) {
$balance = $this->order->getBalance();
if ($include_pending) {
foreach ($this->getPendingAmounts() as $gateway_id => $amounts) {
foreach ($amounts as $amount) {
$balance->subtract($amount);
}
}
}
return $balance && !$balance->isZero();
}
/**
* {@inheritdoc}
*/
public function getPendingAmounts(): array {
$query = $this->entityTypeManager
->getStorage('commerce_payment')
->getAggregateQuery();
$query
->condition('order_id', $this->order->id())
->condition('state', 'pending');
$query
->aggregate('amount.number', 'SUM')
->groupBy('amount.currency_code')
->groupBy('payment_gateway');
$pending_amounts = $query->execute();
$pending = [];
foreach ($pending_amounts as $amount) {
$pending[$amount['payment_gateway']][] = new Price($amount['amountnumber_sum'], $amount['amount__currency_code']);
}
return $pending;
}
/**
* {@inheritdoc}
*/
public function needsConfirmation() {
$transitions = $this->order->getState()->getTransitions();
return isset($transitions['place']);
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
// The summary has no actions.
if ($form['#step_id'] == 'summary') {
return [];
}
$steps = $this->getVisibleSteps();
$next_step_id = $this->getNextStepId($form['#step_id']);
$previous_step_id = $this->getPreviousStepId($form['#step_id']);
$has_next_step = $next_step_id && isset($steps[$next_step_id]['next_label']);
$has_previous_step = $previous_step_id && isset($steps[$previous_step_id]['previous_label']);
$actions = [
'#type' => 'actions',
'#access' => $has_next_step,
];
if ($has_next_step) {
if ($next_step_id == 'payment' && !$this->needsPayment() && !$this->needsConfirmation()) {
$parameters = [
'commerce_order' => $this->order->id(),
'step' => 'summary',
];
$options = [
'attributes' => ['class' => ['btn', 'btn-primary']],
];
$actions['next'] = Link::createFromRoute($this->t('Go to summary'), self::ROUTE_NAME, $parameters, $options)->toRenderable();
}
else {
$label = $steps[$next_step_id]['next_label'];
if ($next_step_id == 'payment' && !$this->needsPayment()) {
$label = $this->t('Confirm my booking');
}
$actions['next'] = [
'#type' => 'submit',
'#value' => $label,
'#button_type' => 'primary',
'#submit' => ['::submitForm'],
];
}
}
if ($has_previous_step) {
$label = $steps[$previous_step_id]['previous_label'];
$parameters = [
'commerce_order' => $this->order->id(),
'step' => $previous_step_id,
];
$options = [
'attributes' => ['class' => ['btn', 'btn-light']],
];
$actions['previous'] = Link::createFromRoute($label, self::ROUTE_NAME, $parameters, $options)->toRenderable();
// Ensure some spacing.
$actions['previous']['#prefix'] = ' ';
}
return $actions;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$redirect = $form_state->getRedirect();
if (!isset($redirect) || ($redirect instanceof Url && $redirect->getRouteName() == 'commerce_checkout.form')) {
$form_state->setRedirect(self::ROUTE_NAME, [
'commerce_order' => $this->order->id(),
'step' => $this->getNextStepId($form['#step_id']),
]);
}
}
/**
* Show payment instructions for any pending payments.
*
* @param \Drupal\commerce_order\Entity\OrderInterface $order
* The order to show messages for.
*/
protected function showPaymentInstructions(OrderInterface $order) {
/** @var \Drupal\commerce_payment\Entity\PaymentInterface[] $payments */
$payments = $this->entityTypeManager
->getStorage('commerce_payment')
->loadByProperties([
'order_id' => $order->id(),
'state' => 'pending',
]);
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
foreach ($payments as $payment) {
$payment_gateway_plugin = $payment->getPaymentGateway()->getPlugin();
if ($payment_gateway_plugin instanceof HasPaymentInstructionsInterface) {
$instructions = $payment_gateway_plugin->buildPaymentInstructions($payment);
if ($instructions) {
$message = $renderer->renderPlain($instructions);
$this->messenger()->addStatus($message);
}
}
}
}
}
