Programmatic Carts and Orders in Ubercart

Here is a (somewhat raw) function that will place an order in ubercart on behalf of a given, fully-loaded account. It all happens in code, bypassing the multiple-screen checkout process of UC.

This means you have to have all the info you need for the order already stored somewhere, of course. In the use case that spawned this code, it is being gathered through a series of custom forms.

Division of UI and API

Ubercart has a very strong coupling between the UI and the API. A lot of functions were still useful, but there are many, many places where global $user, $_SESSION, referrer, and $_POST info are inspected, which makes programmatic work difficult, since the code assumes forms are being submitted directly from the browser.

I'm admittedly pretty new to UC, so maybe there are rationales I'm not seeing, but I think a valuable long-term gain would be to split the API and UI in a matter similar to Views or Imagecache. I realize that's a mammoth undertaking.

Validation versus Submission

I'm also curious why so much of the work is done in the validation handlers as opposed to the submit handlers. Seems they should just validate, not change the data so much. That'll require some investigation on my part, I'm sure there is a reason.

<?php
/*  this simulates, for a given account, the submission of all
    the forms a user would go through from /cart to get through
    UC checkout and payment.

    the checkout process does its heavy lifting in the form
    _validation_ functions instead of the _submit_ functions,
    which generally seem relegated to performing redirects.
   
    this is still a bit raw, as you can see in the error handling
    */

function programmed_uc_order($account, $form_values) {
 
$error = FALSE;

 
// a cart id (cid) is just the uid for existing accounts.
  // you could add any number of items to a cart
 
uc_cart_add_item(ITEM_NID, 1, NULL, $account->uid, FALSE, FALSE, TRUE);

 
$cart_form_state['values']['op'] = t('Checkout');
 
/*  uc_cart_update_item_object is the important part of
      uc_cart_view_form_submit(), the rest is redirection and
      sessions. user would then go off to  cart/checkout where
      they'd enter payment info via uc_cart_checkout_form().
     
      we don't even have to do this because all the modifications
      would do is change cart quantities and we're beyond that.
      
      uc_cart_checkout_form() handily checks the referer so it is
      impossible to call it programmatically. there is a lot of
      display stuff going on in there, but CC info is also being
      gathered. in our case, we already have the CC info in
      storage. */
     
 
$order = uc_order_new($account->uid);
 
$order->products = uc_cart_get_contents($account->uid);
 
 
// this bit is clipped from uc_cart_checkout_form_validate()
 
$context = array(
   
'revision' => 'original',
   
'type' => 'order_product',
  );
  foreach (
$order->products as $key => $item) {
   
$price_info = array(
     
'price' => $item->price,
     
'qty' => $item->qty,
    );
   
$context['subject'] = array(
     
'order' => $order,
     
'product' => $item,
     
'node' => node_load($item->nid),
    );

   
// Get the altered price per unit, as ordered products have a locked-in
    // price. Price altering rules may change over time, but the amount paid
    // by the customer does not after the fact.
   
$price = uc_price($price_info, $context) / $item->qty;
    if (
$order->products[$key]->price != $price) {
     
$order->products[$key]->data['altered_price'] = $price;
    }
  }

 
$order->order_total = uc_order_get_total($order, TRUE);
 
// end clip

 
$panes = _checkout_pane_list();
 
$pane_values = construct_pane_values($account, $form_values);
 
 
// invoke checkout functions for each pane with 'process' op.
 
foreach ($panes as $pane) {
   
$pane_id = $pane['id'];
   
$func = _checkout_pane_data($pane_id, 'callback');
   
$isvalid = $func('process', $order, $pane_values[$pane_id]);
   
// some of these panes REALLY want to make the order belong
    // to global $user. to them we say no.
   
$order->uid = $account->uid;
    if (
$isvalid === FALSE) {
     
$error = $func;
      break;
    }
  }

 
$order->line_items = uc_order_load_line_items($order, TRUE);
 
uc_order_save($order);
 
 
/*next onto /cart/checkout/review which is mostly about
    displaying review info. uc_cart_checkout_review_form_submit()
    is the finalize button on this page. it does some invocations
    for UC 'submit', then redirects the user to
    cart/checkout/complete */

  // clipped from uc_cart_checkout_review_form_submit()
  // Invoke it on a per-module basis instead of all at once.
 
foreach (module_list() as $module) {
   
$function = $module .'_order';
    if (
function_exists($function)) {
     
// $order must be passed by reference.
     
$result = $function('submit', $order, NULL);

     
$msg_type = 'status';
      if (
$result[0]['pass'] === FALSE) {
       
$error = $function;
        break;
      }
    }
  }
 
/*cart/checkout/complete... the actual processing of the order
    doesn't happen so much in the previous submit but in the
    loading of this page depending on a session variable. */
 
uc_cart_complete_sale($order, FALSE);
}

/*  simulate the pane contents for processing.
    this would change a lot depending on how your data is
    structured. here we're pulling info out of various storage
    arrays. the important things are that the pane values get
    populated and $_POST gets the payment info it needs.
    if you're using modules that create their own panes, they'll
    have to be added; this is a pretty vanilla UC install. */

function construct_pane_values($account, $form_values) {
 
$pane_values['customer'] = array(
   
'primary_email' => $account->mail,
  );
 
 
$pay = $form_values['storage']['payment'];

 
$pane_values['billing'] = array(
   
'billing_address_select' => '',
   
'billing_first_name' => '',
   
'billing_last_name' => '',
   
'billing_company' => '',
   
'billing_street1' => $pay['billing_address_1'],
   
'billing_street2' => $pay['billing_address_2'],
   
'billing_city' => $pay['billing_city'],
   
// 840 is the US
   
'billing_country' => 840,
   
// state #
   
'billing_zone' => 1,
   
'billing_postal_code' => $pay['billing_postal_code'],
   
'billing_phone' => '',
  );

 
$pane_values['payment'] = array(
   
'current_total' => NULL,
   
'payment_method' => 'credit',
  );
 
 
// uc_payment_method_credit() inspects $_POST directly for cc info
 
$_POST['cc_type'] = $pay['cctype'];
 
$_POST['cc_owner'] = $pay['ccowner'];
 
$_POST['cc_number'] = $pay['ccnumber'];
 
$_POST['cc_exp_month'] = $pay['ccexpire']['month'];
 
$_POST['cc_exp_year'] = $pay['ccexpire']['year'];
 
// for this example, these fields are not being used.
//        'cc_start_month' => check_plain($_POST['cc_start_month']),
//        'cc_start_year' => check_plain($_POST['cc_start_year']),
//        'cc_issue' => check_plain($_POST['cc_issue']),
//        'cc_cvv' => check_plain($_POST['cc_cvv']),
//        'cc_bank' => check_plain($_POST['cc_bank']),   
 
 
$pane_values['comments'] = array(
   
'comments' => '',
  );

  return
$pane_values;
}
?>

the code doesn't grant

the code doesn't grant access to file uc_file_users.

Hey, this is very nice. It

Hey, this is very nice. It sucks that doing a purchase programmatically with ubercart has to be so complicated. A question: Is the $account parameter in your function a user object such as that returned by user_load() ?