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() ?