My First Fully Integrated Software Shopping Cart

I've always been super interested in point-of-sale systems and e-commerce, particularly the bits around inventory management, shopping carts and really slick sales integration pieces. I've never had a really good functional reason to mess around with any of this, though, outside of when I worked at IBM on an order management system for a client, until I wanted to start selling BatteryTech licenses online. The site, www.batterypoweredgames.com runs Drupal and I had some special needs so with just a little bit of module coding, I was able to piece together a fully automated sales and checkout system that requires a webform to be completed (EULA in my case), handles payment via paypal and provides the customer with access to everything once the payment is completed. This article will outline how I did it.

I'll refer to www.batterypoweredgames.com as BPG and BatteryTech as BT. First of all, BPG runs Drupal v6. I'd love to upgrade to 7 but it's difficult with so many module dependencies that are still only on 6 as of today. With that said, it was easy to find modules to do what I wanted. The first choice is the shopping cart. I chose Ubercart because it seems mature and well-supported. I also like that it has everything a store needs, including products, cart, checkout, payment handling and many, many extra modules.

Installing Ubercart was a snap. I just followed the directions on their site and it was in and working. Adding my first products was easy as well. That just consists of creating new nodes of the 'Product' content type. The real challenge was figuring out how to get my workflow in. Selling software can be a little more difficult than hard goods because of the legalities. BT is particularly challenging because it is a source-included product with very few means of protecting who can get it and use it unauthorized. It doesn't include any kind of a black box or means to prevent piracy. Most everything I do is heavily pirated, but BT would be particularly troublesome because it is much more than just a game or an end-product. My lawyer made it quite clear that I need a good way to have people read the EULA and understand it. He said a checkbox saying "I have read and understood the agreement and agree to the terms" just isn't enough.

My required workflow

Here's how I needed things to happen:
1) User views BT product info
2) User decides to buy BT, so adds the correct BT license product to the cart
3) User checks out
4) User creates an account if not already logged in
5) User agrees to EULA
6) User pays
7) User is given role of "batterytech_licensee" and is granted access to downloads and private forum
8) User makes awesome games of Android and iPhone.

My Solution

Drupal 6
Ubercart module
Webforms module
Node Access Control module
Private Downloads module
Custom checkout pane module

Let me start by saying that I LOVE the webforms module. It is so flexible, you can do just about anything you want with it. I created a form for my EULA. I posted all of the content of the document my lawyer gave me and did a little HTML formatting to make it look right. I then added the following fields to the webform:
Webforms Custom FieldsWebforms Custom Fields

This in itself made it easy to just give new BT licensees a link to the EULA to fill out digitally - no more sending contracts all over the place.

After I had that working, the next challenge was making it so that the checkout process would know if a EULA had been submitted by the current user and stopping the process until the user submitted it. I thought hard about the problem and looked at a number of potential solutions. Eventually I settled on implementing it as a checkout pane. I wrote a custom module for Drupal called uc_reqwebform and implemented the following functions:

form_alter = add validation to checkout
checkout_pane = what will be in our pane (meta)
checkout_form_validate = actually perform validation - stops checkout process
checkout_pane_callback = render to pane, provide link, etc

Here are the notes I took when I was figuring out how this will work:

"EULA on checkout before submit"
add pane to checkout
- if not eula submitted & account created
-- "BatteryTech requires acceptance of a EULA. Click here to read and accept."
--- EULA webform needs logic to go back to order if came from order.
-- "You have accepted the BatteryTech EULA."
- Pane prevents submission (use TOS module as template) if a BT item is in but no EULA submitted by user.

I was able to use the webforms module as a dependency of my own and use its queries to determine if the current user had submitted the form. I'll post most of the code at the end of the article. It took me about a day and a half or may be 2 days to work through all of the php and get this working the way I needed it. I was so excited when it was ready! But I didn't realize something - I would need an SSL certificate (guess that slipped my mind)...

I dug around and found that I would be happy with a GeoTrust certificate, which are $150/yr for the premium one. More digging found that they have a subsidiary, RapidSSL, which are $50/yr. More digging found a reseller, namecheap, which sells them for $10/yr! I bought 3 years worth of SSL glory for under $30. A day later, my certificate arrived and I installed it on my Apache 2.2 server. With everything working, I did some quick Paypal sandbox testing and worked out the configuration kinks. I switched it all on into live/production settings and did a quick 1 cent purchase as a test of the entire workflow. Everything went without a hitch!

Ready for a lot of code? Here's uc_reqwebform.module, which has hard-coded node IDs, content types and URLs based on my own server's configuration. It would be cool if someone made an admin interface for this so that you could configure it to require a webform submission for any given node, but this works great for me for the time being.

The last thing I did was configure a role-assignment module so that after a purchase was made, the conditional action for that module would assign the currently logged-in user the role of batterytech_licensee, which, when combined with the node access module, would allow them to access the private forum, which has the latest distributions of BT on it for private download. Perfect!

Enjoy!

/**
* Ubercart hooks.
*/

/**
* Implementation of hook_checkout_pane().
*/
function uc_reqwebform_checkout_pane() {
$title = t('End-User License Agreement');
if ($node->title) {
$title = $node->title;
}
if (uc_reqwebform_has_products_needing_eula()) {
$panes[] = array(
'id' => 'eula',
'callback' => 'uc_reqwebform_checkout_pane_callback',
'title' => t('@title', array('@title' => $title)),
'desc' => t("EULA Info."),
'weight' => 2,
'collapsible' => FALSE,
);
return $panes;
} else {
return array();
}
}

function uc_reqwebform_has_products_needing_eula() {
$has_products_needing_eula = FALSE;
$cart_cont = uc_cart_get_contents();
$prod_count = count($cart_cont);
if ($prod_count > 0) {
foreach ($cart_cont as $item) {
$item_node = node_load($item->nid);
if ($item_node->type == "batterytech") {
$has_products_needing_eula = TRUE;
}
}
}
return $has_products_needing_eula;
}

/**
* Callback form for checkout pane.
*/
function uc_reqwebform_checkout_pane_callback($op) {
switch ($op) {
case 'view':
$form = array();
if (uc_reqwebform_has_submission()) {
$form['eula_notice_text'] = array(
'#value' => t("EULA has been submitted successfully."),
);
} else {
$form['eula_notice_text'] = array(
'#value' => t("This cart contains products that have required EULAs.
" . l("Click Here for EULA View and Acceptance Form - Required f
or BatteryTech", "batterytech/eula")),
'#prefix' => '* ',
);
}
// $form['#theme'] = 'uc_reqwebform_agreement_form';
return array('contents' => $form);
break;
}
}

/**
* Implementation of hook_form_alter().
*/
function uc_reqwebform_form_alter(&$form, $form_state, $form_id) {
if ($form_id == 'uc_cart_checkout_form') {
if (uc_reqwebform_has_products_needing_eula()) {
$form['#validate'][] = 'uc_reqwebform_checkout_form_validate';
}
return;
}
}

/**
* Validate function for checkout, if required by config.
* This way, we can display a better required message.
*/
function uc_reqwebform_checkout_form_validate($form, &$form_state) {
if (!uc_reqwebform_has_submission()) {
form_set_error('eula_notice_text', t("This cart contains items that require acceptance of an ") . l("End-User License Agreement", "b
atterytech/eula") . ".");
}
}

function uc_reqwebform_has_submission() {
module_load_include('inc', 'webform', 'includes/webform.submissions');
global $user;
// checks for webform submission.
$eula_node = node_load(921);
if ($eula_node) {
$sub_count = webform_get_submission_count($eula_node->nid, $user->uid, TRUE);
if ($sub_count > 0) {
return TRUE;
}
}
return FALSE;
}

/**
* Menu title callback
*/
function uc_reqwebform_title_callback($node = NULL) {
$title = t('EULA');
if ($node->title) {
$title = $node->title;
}
return t('@title', array('@title' => $title));
}

function uc_reqwebform_checkout_pane_alter($panes) {
if (uc_reqwebform_has_products_needing_eula() && !uc_reqwebform_has_submission()) {
// Only show cart and EULA until eula is completed.
foreach ($panes as $key=>$pane) {
if ($pane['id'] != 'cart' && $pane['id'] != 'eula') {
unset($panes[$key]);
}
}
// print_r($panes);
$panes['customer_pane'] = nil;
}
}

// Webform submission hook to bring user back to checkout
function uc_reqwebform_webform_submission_insert($node, $submission) {
if ($node->nid == 921) {
if (count(uc_cart_get_contents()) > 0) {
drupal_goto('cart/checkout');
}
}
}