The Goal
In Drupal Commerce, carts do not have order numbers. Order numbers are not set until checkout completion, when the order is placed. If the order type has been configured with a number pattern, that pattern is used to generate the order number; otherwise the order entity ID is used. As a result, when a credit card payment is added during checkout, the cart/order may not have its final Order Number yet. For some payment gateways, this is fine--only the order entity ID is needed. But for payment gateways like Authorize.net, for which an invoice number is transmitted as part of the payment creation transaction, the absence of the order number can be problematic.
One workaround would be to make a custom api call to the payment gateway to update the invoice number after checkout is complete, but this functionality is not typically provided by Drupal Commerce payment gateway integrations.
Instead, we can customize the checkout flow so that an order number is assigned to the cart just before the api call is made to create the payment transaction. In terms of the default order checkout flow, that means we want to set the order number after the Review step but before the Payment step.
A Solution
We'll create a custom Commerce Checkout Pane plugin in which we programmatically set the order number. Then add our custom "Pre payment Process" pane to the Payment step of our order checkout flow, immediately before the Payment process pane;

For our custom pane, we'll implement the isVisible
method to control whether the pane is active for a specific order. We don't actually need to set the order number if no payment transaction will be made; i.e., if the "Payment process" pane is hidden or no payment gateway is set, then the code in our custom "Pre Payment Process" pane will not be executed.
In the buildPaneForm
method, we first check whether the order already has an order number and do nothing if it does. (Alternatively, this check could be made in the isVisible
method.) To set the number, we need to load the order type entity corresponding to the order's bundle. Then we can use the getNumberPattern
method to get the number pattern, if set. If no number pattern is set, we just set the order number to the order entity id. Otherwise, we use the plugin for the number pattern to generate the number for the order.
After creating the commerce checkout pane plugin, update your order checkout flow configuration to place it right before the "Payment process" pane. (You may need to rebuild caches to make the pane available.)
Implementation:
We'll define our custom checkout plugin class in a custom module, like this:
<?php
namespace Drupal\my_module\Plugin\Commerce\CheckoutPane;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides the pre payment process pane.
*
* @CommerceCheckoutPane(
* id = "my_module_pre_payment_process",
* label = @Translation("Pre Payment Process"),
* display_label = @Translation("Pre Payment Process"),
* wrapper_element = "container",
* default_step="payment"
* )
*/
class PrePaymentProcessPane extends CheckoutPaneBase {
}
Next we'll implement the isVisible
method:
public function isVisible() {
}
We can copy the logic in the "Payment process" checkout pane plugin isVisible
method: Drupal\commerce_payment\Plugin\Commerce\CheckoutPane\PaymentProcess::isVisible
:
if ($this->order->isPaid() || $this->order->getTotalPrice()->isZero()) {
// No payment is needed if the order is free or has already been paid.
return FALSE;
}
$payment_info_pane = $this->checkoutFlow->getPane('payment_information');
if (!$payment_info_pane->isVisible() || $payment_info_pane->getStepId() == '_disabled') {
// Hide the pane if the PaymentInformation pane has been disabled.
return FALSE;
}
Additionally, we'll hide the pane if no payment gateway is set:
if ($this->order->get('payment_gateway')->isEmpty()) {
// Hide the pane if the payment gateway is not set.
return FALSE;
}
The only other method we need is buildPaneForm
. Here is the implementation as described above:
public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) {
if (!$this->order->getOrderNumber()) {
$order_type_storage = $this->entityTypeManager->getStorage('commerce_order_type');
/** @var \Drupal\commerce_order\Entity\OrderTypeInterface $order_type */
$order_type = $order_type_storage->load($this->order->bundle());
/** @var \Drupal\commerce_number_pattern\Entity\NumberPatternInterface $number_pattern */
$number_pattern = $order_type->getNumberPattern();
if ($number_pattern) {
$order_number = $number_pattern->getPlugin()->generate($this->order);
}
else {
$order_number = $this->order->id();
}
$this->order->setOrderNumber($order_number);
}
return $pane_form;
}
Comments