Creating checkout panes for Ubercart

Dev

Creating checkout panes for Ubercart

by Sean Corrales
//

All code comes from a shipping insurance module I wrote for Ubercart. I plan to release it on ubercart.org and drupal.org after I finish up the remaining work on it.

Ubercart is the most robust solution for e-commerce sites running on Drupal today; one of its greatest strengths is the ability to add new functionality to the system using custom modules. This entry will detail the basics of creating a checkout pane that will add a line item to orders and store the data.

Panes
Panes are used throughout Ubercart and can be turned on and off. For this entry, I’ll be working with hook_checkout_pane(). This will add a pane that will be available for use on the checkout screen.

/**
*
* Implementation of hook_checkout_pane()
*
*/
function uc_shipping_insurance_checkout_pane(){
   $panes[] = array(
     'id' => 'shipping_insurance',
     'title' => 'Purchase optional insurance',
     'desc' => 'Provide customers with the option to purchase shipping insurance',
     'callback' => 'uc_shipping_insurance_pane_checkout',
     'process' => TRUE,
     'enabled' => FALSE,
     'weight' => 1,
   );

   return $panes;
}

Your implementation of hook_checkout_pane will provide some information about your pane as well as registering a callback function for generating the pane. Here’s my pane rendering function; you’ll notice it uses the same name as the value we enter for the callback in our hook_checkout_pane()

/**
*
* Checkout pane function
*
*/
function uc_shipping_insurance_pane_checkout($op, $arg1, $arg2){
   //If no products are shippable, exit funciton
   $ship = uc_cart_is_shippable();
   if(!$ship) return;

   switch($op){
    
     //CHECKOUT FORM
     case 'view':
       //Return all products in cart; run through list and total up cost of shippable items.
       $products = uc_cart_get_contents();
       $total = 0;

       foreach($products as $product){
         if($product->data['shippable']){
           $total += $product->qty * $product->price;
         }
       }
       $percentage = 0.01;
       $insurance = round($total * $percentage, 2);
      
       $context = array(
             'revision' => 'themed-original',
            'type' => 'amount',
           );      
      
       drupal_add_js(drupal_get_path('module', 'uc_shipping_insurance') .'/uc_shipping_insurance.js');
  
       $description = "Would you like shipping insurance?";
      
       $contents['insurance_cost'] = array(
         '#value' => '

Shipping insurance: '.
          uc_price($insurance, $context)
          .'

',
       );
      
       $contents['insurance'] = array(
         '#type' => 'radios',
         '#options' => array(
           '2' => 'Yes, I want to purchase optional shipping insurance which insures my shipment to full value',
           '1' => 'No, I do not want to buy optional shipping insurance and I understand that if my package is lost in transit, I will only receive a maximum claim of $100',
         ),
         '#required' =>  TRUE,
         '#title' => 'Shipping Insurance',
         '#default_value' => $arg1->shipping_insurance['accept'] ? $arg1->shipping_insurance['accept'] : NULL,
       );
      
       $contents['cost'] = array(
         '#type' => 'hidden',
         '#value' => $insurance,
       );
      
       $contents['percentage'] = array(
         '#type' => 'hidden',
         '#value' => $percentage,
       );

       return array('description' => $description, 'contents' => $contents);
    
     //CHECKOUT PROCESSING - load values into order object.
     case 'process':
       $arg1->shipping_insurance = array(
         'accept' => $arg2['insurance'],
         'cost' =>  $arg2['cost'],
       );
       return TRUE;
    
     //CHECKOUT REVIEW 
     case 'review':   
        $message[0] = array('title' => 'Shipping Insurance');
 
          if($arg1->shipping_insurance['accept'] == '2'){
           $message[0]['data'] = 'Accepted';
          $message[] = array(
              'title' => 'Cost',
             'data' => '$' . $arg1->shipping_insurance['cost'],
           );
           } else {
             $message[0]['data'] = 'Declined';
        }
         return $message;
   }
}

The rendering function takes the arguments $op, $arg1, and $arg2. You can read more about the different operations here. The three ops that we’ll go over are view, process, and review.

View
This is when you’re rendering the form on the checkout screen. In this operation, you’ll need to return an associative array containing the keys description and contents. Description is a string, contents is a Drupal form array.

Process
During this phase, you can modify the order object ($arg1) and add any necessary values. If these values need to be stored in the database, you will need to implement hook_order() to store the data. An example of hook_order can be found below.

Review
This is when the customer is reviewing their submitted information before submitting their order for processing. You can return a number of different things and you can read about them in more detail here.

Saving your data
Information can be added to the order object during your pane rendering function’s process operation but in order for it to persist, it must be saved to the database. In order to do that, we’ll implement hook_order.

/**
*
* Implementation of hook_order
*
* Responsible for storing, loading, and deleting shipping insurance data.
*
*/
function uc_shipping_insurance_order($op, &$arg1, $arg2){
   switch($op){ 
   
     //Save insurance data to database
     case 'save':
       if(!empty($arg1->shipping_insurance['accept'])){
         db_query("UPDATE {uc_shipping_insurance} SET uid = '%d', accept = '%d', cost = '%f' WHERE oid = '%d' LIMIT 1", $arg1->uid,
             $arg1->shipping_insurance['accept'], $arg1->shipping_insurance['cost'], $arg1->order_id);
        
         //If no rows, need to insert new row
         if(db_affected_rows()            db_query("INSERT INTO {uc_shipping_insurance} (oid, uid, accept, cost) VALUES (%d, %d, %d, '%f')", $arg1->order_id, $arg1->uid, $arg1->shipping_insurance['accept'], $arg1->shipping_insurance['cost']);
         }
        
         //Add/remove item from line item table
         $item['amount'] = $arg1->shipping_insurance['cost'];
      
         if($arg1->shipping_insurance['accept'] == 2){
           _uc_shipping_insurance_line_item($arg1->order_id, $item);
         } else {
           _uc_shipping_insurance_line_item($arg1->order_id, $item, 'delete');
         }
       }
       break;
    
     //Load insurance data
     case 'load':
       $result = db_query("SELECT * FROM {uc_shipping_insurance} WHERE oid = %d", $arg1->order_id);
       if($row = db_fetch_object($result)){
         $arg1->shipping_insurance['accept'] = $row->accept;
         $arg1->shipping_insurance['cost'] = $row->cost;
       }
       break;
    
     //Delete insurance data
     case 'delete':
       db_query("DELETE FROM {uc_shipping_insurance} WHERE oid = %d", $arg1->order_id);
       break;
   }
}

To store this data, you will need to create a simple table for your module and then provide a switch supporting the save, load, and delete ops. In each of these ops, you will save, load, and delete the data as necessary. This is how you ensure your information is stored and available in the order object.

Adding line items
The final component has to do with altering the total order price with line items. Line items appear after the subtotal and are added up to create the order total. For example, tax and shipping would be considered line items.

Before you can render line items, you must first register a callback function with hook_line_item(). This function follows a similar format to hook_checkout_pane().

/**
*
* Implementation of hook_line_item
*
*/
function uc_shipping_insurance_line_item(){
   $items[] = array(
     'id' => 'shipping_insurance',
     'title' => 'Shipping Insurance',
     'weight' => 2,
     'default' => TRUE,
     'stored' => TRUE,
     'add_list' => FALSE,
     'calculated' =>  TRUE,
     'display_only' => FALSE,
   );
  
   return $items;
}

Make note, the ID you set in hook_line_item must be used when adding and removing line items.

There are two ways you can add line items and both have an important role in any module adding line items during checkout.

In realtime
When a customer fills out their information and selects shipping options, there is a dynamically updated HTML div of content in the payment pane that is constantly recalculating the total order price as different line items are removed or added. This display is only for the end user; none of the data is stored or persists beyond this screen. However, to prevent any surprises, it’s important to update the order total as we add additional fees. We do this using jQuery.

  //Bind actions to add/remove line items as user accepts/declines insurance.
  $('#shipping_insurance-pane .form-radios input').change( function(){
   if($(this).val() == 2){
     set_line_item('shipping_insurance', 'Shipping Insurance', insurance, 2);
   } else {
     remove_line_item('shipping_insurance');
   }
  });
 
  //Checks to see if the box is already checked and adds to line items
  if($('#shipping_insurance-pane .form-radios input:checked').val() == 2){
    set_line_item('shipping_insurance', 'Shipping Insurance', insurance, 2);
  }

We’re doing a few things here: we bind some functionality to the form items to update the line items and then we implement a simple check to add the line item on page load if a value has already been selected. A case where this might happen is when the user clicks “Back” when on the checkout review page.

Regardless of the code that calls them, the functions that are of most interest are set_line_item() and remove_line_item(). You can read more about them on their respective API pages but the gist of their functionality is they add or remove your line item and then recalculate and update the order total on the page.

Storing line items
In order to have your line items saved and affect the actual order total, you have to insert them into the uc_order_line_items table. There are three functions that make working with the database table easier: uc_order_line_item_add, uc_order_update_line_item, and uc_order_delete_line_item.

Your code will need to determine when to call these functions; there is nothing to prevent your module from adding the same line item to an order 20 times. Your code has to ascertain when to insert, when to update, and when to delete.

The order object contains a property called line_items. This property is an array storing data about all the existing line items when the order was loaded from the database. Using this, you can determine if your line item should be added or updated.

Personally, I find this tedious and created a wrapper function for these functions. It runs a single DB query to determine if the order should be updated or deleted. It also saves unnecessary updates by checking for changed values.

/**
*
* Function to determine if line item exists and act approriately.
*
* @param oid - order id you wish to modify
* @param item - array of data to be inserted/updated. Should include amount
* @param action - can be 'insert', 'update', or 'delete'. By default, will do insert on zero results or update if necessary
*
*/
function _uc_shipping_insurance_line_item($oid, $item = array(), $action = NULL){
   //Set these as defaults since we don't want to start modifying enrtries not related to this module.
   $item['title'] = 'Shipping Insurance';
   $item['type'] = 'shipping_insurance';
   $item['weight'] = 2;
  
   //Query to fetch matching row
   $line_items = db_query("SELECT line_item_id, amount FROM {uc_order_line_items} WHERE order_id = %d AND type = '%s'", $oid, $item['type']);
  
   //If no result and not deleting, insert
   if(db_affected_rows()      uc_order_line_item_add($oid, $item['type'], t($item['title']), $item['amount'], $item['weight']);  
      
   } else {
     //We have an existing row - need to update or delete
     $row = db_fetch_array($line_items);
    
     if($action == 'delete') {
       uc_order_delete_line_item($row['line_item_id']);
     } else {
       //If the amount has changed, update. Else, just pass on by.
       if($row['amount'] != $item['amount']) uc_order_update_line_item($row['line_item_id'], $item['title'], $item['amount']);
     }
   }
  
}

This function could be reworked to traverse the entire array of line items from the order object to look for the line item id. I felt that this was a quicker solution to implement.

With an entry in the uc_order_line_item table from our module, we’ve now affected the total of the order.

More About the Author

Sean Corrales

Lead Web Developer
Internet Explorer ignoring CSS files Like most web developers, I do most of my development work in one browser (in my case, Firefox) and then do cross browser checks after ...
Creating checkout panes for Ubercart All code comes from a shipping insurance module I wrote for Ubercart. I plan to release it on ubercart.org and drupal.org after I ...

See more from this author →

Subscribe to our newsletter

  • I understand that InterWorks will use the data provided for the purpose of communication and the administration my request. InterWorks will never disclose or sell any personal data except where required to do so by law. Finally, I understand that future communications related topics and events may be sent from InterWorks, but I can opt-out at any time.
  • This field is for validation purposes and should be left unchanged.

InterWorks uses cookies to allow us to better understand how the site is used. By continuing to use this site, you consent to this policy. Review Policy OK

×

Interworks GmbH
Ratinger Straße 9
40213 Düsseldorf
Germany
Geschäftsführer: Mel Stephenson

Kontaktaufnahme: markus@interworks.eu
Telefon: +49 (0)211 5408 5301

Amtsgericht Düsseldorf HRB 79752
UstldNr: DE 313 353 072