Skip to main content

Drupal 8 Commerce 2, create an order programmatically

Friday 13 de March de 2020

In some situations we may need to create an order using custom code. For example, if we want to create an order quickly after a user action without having to go through the entire process (searching for the product, selecting units and adding them to the cart) or if we want to hide the Commerce interface and use a custom interface.

Where to start the process

The first thing is to determine where to add the code that creates the order. This will depend on what action the order will be created in response to. Normally we'll create our own Drupal service to do the job and call that service from where we want to create the order. Using the service we can also easily use dependency injection, which is the best way to add external dependencies to our classes.

Create the order

In the file in charge of adding the order we must import the following classes:

use Drupal\commerce_price\Price;
use Drupal\commerce_order\Entity\OrderItem;
use Drupal\commerce_order\Entity\Order;

Order item

First we need to create an order item and save it:

$order_item = OrderItem::create([
  'type' => 'awesome_order_item_type',
  'purchased_entity' => 1,
  'quantity' => 1,
  // Omit these lines to preserve original product price.
  'unit_price' => new Price(80, 'EUR'),
  'overridden_unit_price' => TRUE,
]);
$order_item->save();

Order

Then create order and attach the previous order item generated:

$order = Order::create([
  'type' => 'donation',
  'mail' => \Drupal::currentUser()->getEmail(),
  'uid' => \Drupal::currentUser()->id(),
  'store_id' => 1,
  'order_items' => [$order_item],
  'placed' => \Drupal::time()->getCurrentTime(),
  'payment_gateway' => 'example_payment',
  'checkout_step' => 'payment',
  'state' => 'draft',
]);

Recalculate order total and save order:

$order->recalculateTotalPrice();
$order->save();

Add order number and save (based in order id):

$order->set('order_number', $order->id());
$order->save();

Anonymous cart session

For anonymouse users Commerce links the cart with the user session. Because we have added the order using custom code we need to explicitly do the same process if we want to send the user to the checkout. For this, we'll need commerce cart session service in order to provide user the correct access to view this order payment (checkout path, @see \Drupal\commerce_checkout\Controller\ CheckoutController::checkAccess).

$cart_session = \Drupal::service('commerce_cart.cart_session');
if (\Drupal::currentUser()->isAnonymous()) {
  $cart_session->addCartId($order->id());
}

In case of not doing so, the anonymous user will receive an error when accessing the payment.

For registered users none action is needed.

Redirect

After creating the order the usual case is to send the user to the checkout.  As we'll normally have created the request in a form submit in response to a user action we'll use the redirect mechanism in the form submission flow, which would be as follows:

$form_state->setRedirect('commerce_checkout.form', ['commerce_order' => $order->id()]);

That's it, now user will be directly in the checkout workflow.

Additional useful snippets related

Prevent user to add multiple orders to session  (that is, if for example there is already some product in the cart because it has been left halfway through a previous process), one good strategy is to move to the canceled state all previous not completed orders in cart session (type active):

$cart_session = \Drupal::service('commerce_cart.cart_session');
$current_orders = $cart_session->getCartIds();
foreach ($current_orders as $order_id) {
  $order = Order::load($order_id);
  $order->state = 'canceled';
  $order->save();
  // Use just this to remove from cart session.
  $cart_session->deleteCartId($order_id);
}

After all (after redirecting in a custom "CheckoutPane" for example), you can destroy generated user session if the "completed" page does not require any special access, this means a different one of checkout complete default path (that way user can keep browsing cached version of site). This way Drupal can serve cached pages to the user if is anonymous (because the user session prevents Drupal to use cached pages). 

if (\Drupal::currentUser()->isAnonymous()) {
  \Drupal::service('session_manager')->destroy();
}

 

Commerce