by Bill Pirkle

Today’s GSATi blog post takes a technical turn, as we describe a solution we recently put into place for one of our clients, usinga custom Drupal module.

Our client had recently put a shipping promotion into place, where orders over a certain threshold received free ground shipping.  The promotion worked fine in most cases, but one order over the threshold was charged the normal shipping price.  A quick smoke test confirmed the promotion to be working perfectly for new orders.  What had gone wrong?

This client’s store has a traditional three-step checkout based on Drupal Commerce Kickstart.

1) Contact Information (billing and shipping addresses, etc.)

2) Shipping (nine possible methods, automatically filtered by destination)

3) Review and Payment

Examining the order history of the affected order showed that it had been placed into Checkout: Review status before the shipping promotion went into effect.  Therefore, the shipping line item had already been added to the commerce order object.  Because the Drupal Commerce checkout allows the customer to continue where they left off, the customer had simply completed checkout with the erroneous shipping line item still on their order.

The immediate easy solution was to move all older orders that were in Checkout: Review status to Checkout: Checkout status using Views Bulk Operations.  That was completed quickly, resolving the issue for this promotion.  But what about next time? 

We considered the Commerce Cart Expiration module , which is mature and includes some nice functionality.  I encourage you to use it if it fits your needs.  However, Commerce Cart Expiration is built around the idea of deleting abandoned carts.  While this makes sense for a lot of stores, it didn’t work well for this one.  A common use case for this particular client that customers will build an order over time before making a purchase.  It isn’t uncommon for larger orders to sit in various stages of completion for several days before checkout is completed.  We didn’t want to delete these orders or make this style of shopping inconvenient for the customer.

Instead, we decided to create a simple custom module, which we called Commerce Order Timeout, and which is currently a sandbox module on

The module itself is straightforward.  We define one new permission, provide a settings form, and implement a few helper functions.  All that is mostly boilerplate and I won’t include it here.  Download the sandbox module if you’re interested. 

The interesting part of the module consists of two function, a cron hook and a worker function:

function commerce_order_timeout_cron() {

  $timeout_sec = commerce_order_timeout_get_timeout_sec();

  $max_num_orders = commerce_order_timeout_get_max_num_orders();

  $dest_status = commerce_order_timeout_get_dest_status();

   if ($timeout_sec > 0) {

    $order_ids = commerce_order_timeout_get_timed_out_orders($timeout_sec, $max_num_orders);

    if (!empty($order_ids)) {

      $orders = commerce_order_load_multiple($order_ids);

      foreach ($orders as $order) {

        $order->status = $dest_status;






  function commerce_order_timeout_get_timed_out_orders($timeout_sec, $max_num_orders = 0) {

  $target_statuses = commerce_order_timeout_get_target_statuses();


  $query = new EntityFieldQuery();


    ->entityCondition('entity_type', 'commerce_order', '=')

    ->propertyCondition('status', $target_statuses, 'IN')

    ->propertyCondition('changed', REQUEST_TIME - $timeout_sec, '<');

   if ($max_num_orders) {

    $query->range(0, $max_num_orders);


   $result = $query->execute();

   return !empty($result) ? array_keys(reset($result)) : array();


The module performs an EntityFieldQuery to retrieve any orders that need adjustment.  It then resaves the affected orders.  Site administrators may specify a timeout of zero to effectively disable the module.  They may also specify a maximum number of orders to process per cron run, to manage load, or specify zero for unlimited.

Some obvious opportunities for improvement include:

  • Using a rules-based system for easier extendibility
  • Messages informing the customer that a status adjustment is impending
  • A button to check for timed out orders immediately, rather than waiting for cron
  • Multiple mappings (ex. map Checkout: Review => Cart and map Shipped => Exception, right now you can only map to one destination status)

For today, though, we have an automatic system to prevent an entire category of customer service issues, which will hopefully make both our client and their customers a little happier.

Nitie Atamenwan