Index: docs/hooks.php
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ubercart/docs/hooks.php,v
retrieving revision 1.1.2.12
diff -u -p -r1.1.2.12 hooks.php
--- docs/hooks.php	22 Apr 2009 17:45:10 -0000	1.1.2.12
+++ docs/hooks.php	29 Jun 2009 02:33:26 -0000
@@ -657,6 +657,8 @@ function hook_line_item() {
  *       @code
  *         return array(array('pass' => FALSE, 'message' => t('We were unable to process your credit card.')));
  *       @endcode
+ *   - approved: For processes that run immediately after payment approval,
+ *       or rather a successful invocation of $op=submit
  *   - can_update: Called before an order's status is changed to make sure the
  *       order can be updated. $arg1 is the order object with the old order
  *       status ID ($arg1->order_status), and $arg2 is simply the new order
Index: payment/uc_authorizenet/uc_authorizenet.admin.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ubercart/payment/uc_authorizenet/Attic/uc_authorizenet.admin.inc,v
retrieving revision 1.1.2.2
diff -u -p -r1.1.2.2 uc_authorizenet.admin.inc
--- payment/uc_authorizenet/uc_authorizenet.admin.inc	28 Apr 2009 21:03:15 -0000	1.1.2.2
+++ payment/uc_authorizenet/uc_authorizenet.admin.inc	29 Jun 2009 02:33:26 -0000
@@ -7,7 +7,9 @@
  */
 
 
-// Displays a form to update a subscriptions's CC info.
+/**
+ * Displays a form to update a subscriptions's CC info.
+ */
 function uc_authorizenet_arb_admin_update_form($form_state, $rfid) {
   $order = new stdClass();
   $form = array();
@@ -19,7 +21,7 @@ function uc_authorizenet_arb_admin_updat
     '#value' => $rfid,
   );
   $form['description'] = array(
-    '#value' => '<div>'. t('Subscription ID: @subscription_id', array('@subscription_id' => $fee['data'])) .'</div>',
+    '#value' => '<div>'. t('Authorize.net ARB Subscription ID: @subscription_id', array('@subscription_id' => $fee->data['subscriptionId'])) .'</div>',
   );
 
   $form['cc_data'] = array(
@@ -42,6 +44,7 @@ function uc_authorizenet_arb_admin_updat
 
 function uc_authorizenet_arb_admin_update_form_submit($form, &$form_state) {
   $fee = uc_recurring_fee_load('user', $form_state['values']['rfid']);
+  $subscription_id = $fee->data['subscriptionId'];
 
   $updates = array(
     'payment' => array(
@@ -52,7 +55,7 @@ function uc_authorizenet_arb_admin_updat
     ),
   );
 
-  $result = uc_authorizenet_arb_update($fee['data'], $updates, $fee['order_id']);
+  $result = uc_authorizenet_arb_update($subscription_id, $updates, $fee->order_id);
 
   // If the update was successful...
   if ($result) {
@@ -65,27 +68,33 @@ function uc_authorizenet_arb_admin_updat
   $form_state['redirect'] = 'admin/store/orders/recurring';
 }
 
-// Displays a confirm form for cancelling a subscription.
+
+/**
+ * Displays a confirm form for cancelling a subscription.
+ */
 function uc_authorizenet_arb_admin_cancel_form($form_state, $rfid) {
+  $fee = uc_recurring_fee_load('user', $rfid);
+
   $form['rfid'] = array(
     '#type' => 'value',
     '#value' => $rfid,
   );
 
-  return confirm_form($form, t('Are you sure you wish to cancel this subscription?'), 'admin/store/orders/recurring', NULL, t('Confirm'), t('Cancel'));
+  return confirm_form($form, t('Are you sure you wish to cancel this subscription? (ARB ID !subscriptionId)', array('!subscriptionId'=>$fee->data['subscriptionId'])), 'admin/store/orders/recurring', NULL, t('Confirm'), t('Cancel'));
 }
 
 function uc_authorizenet_arb_admin_cancel_form_submit($form, &$form_state) {
   $fee = uc_recurring_fee_load('user', $form_state['values']['rfid']);
+  $subscription_id = $fee->data['subscriptionId'];
 
-  $result = uc_authorizenet_arb_cancel($fee['data'], $fee['order_id'], $fee);
+  $result = uc_authorizenet_arb_cancel($subscription_id, $fee->order_id, $fee);
 
   // If the cancellation was successful...
   if ($result) {
-    drupal_set_message(t('Subscription cancelled through Authorize.Net.'));
+    drupal_set_message(t('Subscription !subscriptionId cancelled through Authorize.Net.', array('!subscriptionId'=>$subscription_id)));
 
     // Set the fee's recurring charges to 0.
-    uc_recurring_fee_cancel($fee['rfid']);
+    uc_recurring_fee_cancel($fee->rfid);
   }
   else {
     drupal_set_message(t('Subscription cancellation failed. See order admin comments for more details.'), 'error');
Index: payment/uc_authorizenet/uc_authorizenet.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ubercart/payment/uc_authorizenet/Attic/uc_authorizenet.install,v
retrieving revision 1.1.2.7
diff -u -p -r1.1.2.7 uc_authorizenet.install
--- payment/uc_authorizenet/uc_authorizenet.install	24 Feb 2009 15:35:50 -0000	1.1.2.7
+++ payment/uc_authorizenet/uc_authorizenet.install	29 Jun 2009 02:33:26 -0000
@@ -26,9 +26,39 @@ function uc_authorizenet_requirements($p
   return $requirements;
 }
 
+function uc_authorizenet_schema() {
+  $schema = array();
+
+  $schema['uc_authorizenet_arb'] = array(
+    'description' => t('Tying Authorize.net ARB subscriptions to recurring fees'),
+    'fields' => array(
+      'rfid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'arb_subscription_id' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('rfid','arb_subscription_id'),
+  );
+
+  return $schema;
+}
+
+function uc_authorizenet_install() {
+  drupal_install_schema('uc_authorizenet');
+}
 function uc_authorizenet_uninstall() {
   // Delete related variables all at once.
   db_query("DELETE FROM {variable} WHERE name LIKE 'uc_authnet_%%'");
+
+  drupal_uninstall_schema('uc_authorizenet');
 }
 
 function uc_authorizenet_update_1() {
@@ -91,3 +121,15 @@ function uc_authorizenet_update_3() {
   return array();
 }
 
+
+function uc_authorizenet_update_6001() {
+  $ret = array();
+
+  if (db_table_exists('uc_authorizenet_arb')) {
+    db_drop_table($ret, 'uc_authorizenet_arb');
+  }
+  db_create_table($ret, 'uc_authorizenet_arb', drupal_get_schema_unprocessed('uc_authorizenet', 'uc_authorizenet_arb'));
+
+  return $ret;
+}
+
Index: payment/uc_authorizenet/uc_authorizenet.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ubercart/payment/uc_authorizenet/uc_authorizenet.module,v
retrieving revision 1.4.2.8
diff -u -p -r1.4.2.8 uc_authorizenet.module
--- payment/uc_authorizenet/uc_authorizenet.module	28 Apr 2009 21:03:15 -0000	1.4.2.8
+++ payment/uc_authorizenet/uc_authorizenet.module	29 Jun 2009 02:33:27 -0000
@@ -17,7 +17,7 @@ function uc_authorizenet_menu() {
 
   $items['authnet/silent-post'] = array(
     'page callback' => 'uc_authorizenet_silent_post',
-    'access callback' => 'uc_authorizenet_silent_post_access',
+    'access callback' => TRUE,
     'type' => MENU_CALLBACK,
     'file' => 'uc_authorizenet.pages.inc',
   );
@@ -65,10 +65,6 @@ function uc_authorizenet_menu() {
   return $items;
 }
 
-// Make sure Authorize.Net always has access to send Silent POSTs.
-function uc_authorizenet_silent_post_access() {
-  return TRUE;
-}
 
 /**
  * Implementation of hook_payment_gateway().
@@ -213,21 +209,27 @@ function uc_authorizenet_payment_gateway
   }
 }
 
-// Main handler for processing credit card transactions.
+
+/**
+ * Main handler for credit card transactions, usually invoked by uc_payment_process() via uc_credit_order('submit')
+ */
 function uc_authorizenet_charge($order_id, $amount, $data) {
-  // Load the order.
-  $order = uc_order_load($order_id);
+  $data['amount'] = $amount;
+  $data['order'] = uc_order_load($order_id);
+
+  // Give modules a chance to alter the nature of this transaction
+  drupal_alter('authorizenet_charge', $data);
 
   // Perform the appropriate action based on the transaction type.
   switch ($data['txn_type']) {
     // Reference transactions are handled through Authorize.Net's CIM.
     case UC_CREDIT_REFERENCE_TXN:
-      return _uc_authorizenet_cim_profile_charge($order, $amount, $data);
+      return _uc_authorizenet_cim_profile_charge($data);
 
     // Set a reference only.
     case UC_CREDIT_REFERENCE_SET:
       // Return the error message if this failed.
-      if ($message = _uc_authorizenet_cim_profile_create($order)) {
+      if ($message = _uc_authorizenet_cim_profile_create($data['order'])) {
         return array('success' => FALSE, 'message' => $message);
       }
       else {
@@ -236,7 +238,7 @@ function uc_authorizenet_charge($order_i
 
     // Accommodate all other transaction types.
     default:
-      return _uc_authorizenet_charge($order, $amount, $data);
+      return _uc_authorizenet_charge($data);
   }
 }
 
@@ -295,9 +297,11 @@ function _uc_authorizenet_cim_profile_cr
 /**
  * Use a reference to charge to a CIM profile.
  */
-function _uc_authorizenet_cim_profile_charge($order, $amount, $data) {
+function _uc_authorizenet_cim_profile_charge($data) {
   global $user;
   $server = variable_get('uc_authnet_cim_mode', 'disabled');
+  $order = $data['order'];
+  $amount = $data['amount'];
 
   // Help build the request.
   $request = _uc_authorizenet_cim_profile_charge_request($order, $amount, $data);
@@ -416,8 +420,10 @@ function _uc_authorizenet_cim_payment_pr
 /**
  * Handles authorizations and captures through AIM at Authorize.Net
  */
-function _uc_authorizenet_charge($order, $amount, $data) {
+function _uc_authorizenet_charge($data) {
   global $user;
+  $order = $data['order'];
+  $amount = $data['amount'];
 
   // Build a description of the order for logging in Auth.Net.
   $description = array();
@@ -431,6 +437,7 @@ function _uc_authorizenet_charge($order,
   $context = array(
     'revision' => 'formatted-original',
     'location' => 'authorize.net-charge',
+    'data' => $data,
   );
   $options = array(
     'sign' => FALSE,
@@ -540,6 +547,8 @@ function _uc_authorizenet_charge($order,
     $response[$i] = substr($response[$i], 1, strlen($response[$i]) - 2);
   }
 
+  //drupal_set_message(kpr(array($post_url, $data, $submit_data, $response), true));
+
   /**
    * Response key index:
    * 0 = Response Code
@@ -616,45 +625,142 @@ function _uc_authorizenet_charge($order,
   return $result;
 }
 
+
+
+
+
+
 /**
- * Implementation of hook_recurring_fee().
+ * Callback function for setting up a recurring fee
  */
-function uc_authorizenet_recurring_fee($order, $fee) {
+function uc_recurring_authorizenet_fee($op, &$fee, $order) {
+  if (variable_get('uc_authnet_arb_mode', 'disabled') != 'disabled') {
+    return uc_recurring_authorizenet_arb_fee($op, $fee, $order);
+  }
+
   // Don't process the fee if ARB is disabled in the gateway settings.
-  if (variable_get('uc_authnet_arb_mode', 'disabled') == 'disabled') {
+  else {
+    // @todo: is it true then that one can't process recurring fees using authorizenet unless they're enrolled in ARB?
     return FALSE;
   }
+}
+
+function uc_recurring_authorizenet_arb_fee($op, &$fee, $order) {
+  switch($op) {
+    case 'create':
+      $fee->fee_handler = 'authorizenet_arb';
+      if ($result = uc_authorizenet_arb_create($order, $fee->fee_amount, $fee->initial_charge, $fee->regular_interval, $fee->number_intervals, $fee)) {
+        $fee->data = array_merge((array)$fee->data, $result);
+        return $result;
+      }
+    case 'created':
+      return uc_authorizenet_arb_create_after($order, $fee->rfid, $fee->data['subscriptionId'], $fee);
 
-  return uc_authorizenet_arb_create($order, $fee);
+    case 'renew':
+      // Renewal is happened remotely so this always is "true"
+      return TRUE;
+
+    case 'delete':
+    case 'cancel':
+      return uc_authorizenet_arb_cancel($fee->arb_subscription_id, $fee->order_id);
+  }
 }
 
+
 /**
- * Implementation of hook_recurring_fee_ops().
+ * Callback for uc_recurring_fee_ops().
  */
-function uc_authorizenet_recurring_fee_ops($context, $fee) {
-  $ops = array();
+function uc_recurring_authorizenet_fee_ops($context, &$fee) {
+  if (variable_get('uc_authnet_arb_mode', 'disabled') != 'disabled') {
+    return uc_recurring_authorizenet_arb_fee_ops($context, $fee);
+  }
+  else {
+    return FALSE;
+  }
+}
 
+function uc_recurring_authorizenet_arb_fee_ops($context, &$fee) {
+  $ops = array();
   switch ($context) {
     case 'fee_admin':
-      if ($fee['remaining_intervals'] > 0) {
-        $ops[] = l(t('update'), 'admin/store/orders/recurring/'. $fee['rfid'] .'/arb-update');
-        $ops[] = l(t('cancel'), 'admin/store/orders/recurring/'. $fee['rfid'] .'/arb-cancel');
+      if ($fee->remaining_intervals > 0) {
+        $ops[] = l(t('update'), 'admin/store/orders/recurring/'. $fee->rfid .'/arb-update');
+        $ops[] = l(t('cancel'), 'admin/store/orders/recurring/'. $fee->rfid .'/arb-cancel');
       }
       else {
-        $ops[] = l(t('delete'), 'admin/store/orders/recurring/'. $fee['rfid'] .'/delete');
+        $ops[] = l(t('delete'), 'admin/store/orders/recurring/'. $fee->rfid .'/delete');
       }
       break;
 
     case 'user':
-      $ops[] = l(t('update'), 'user/'. $fee['uid'] .'/recurring/'. $fee['rfid'] .'/arb-update');
-      $ops[] = l(t('cancel'), 'user/'. $fee['uid'] .'/recurring/'. $fee['rfid'] .'/arb-cancel');
+      $ops[] = l(t('update'), 'user/'. $fee->uid .'/recurring/'. $fee->rfid .'/arb-update');
+      $ops[] = l(t('cancel'), 'user/'. $fee->uid .'/recurring/'. $fee->rfid .'/arb-cancel');
       break;
   }
+  return $ops;
+}
 
 
-  return $ops;
+/**
+ * Implementation of hook_recurring_fee_api()
+ */
+function uc_authorizenet_recurring_fee_api($op, &$fee, &$arg = NULL) {
+  switch ($op) {
+    case 'load':
+      if ($fee->rfid) {
+        $fee->arb_subscription_id = db_result(db_query("SELECT arb_subscription_id FROM {uc_authorizenet_arb} WHERE rfid=%d", $fee->rfid));
+      }
+      break;
+  }
+}
+
+/**
+ * Implementation of hook_authorizenet_silent_post()
+ */
+function uc_authorizenet_authorizenet_silent_post($data) {
+  $previous_order_id = $data['x_invoice_num'];
+  $arb_subscription_id = $data['x_subscription_id'];
+  $previous_order = uc_order_load($previous_order_id);
+  $success = FALSE;
+
+  if ($previous_order->order_id) {
+    $result = db_query("SELECT rf.rfid FROM {uc_recurring_users} rf JOIN {uc_authorizenet_arb} arb ON arb.rfid = rf.rfid WHERE arb.arb_subscription_id=%d", $arb_subscription_id);
+
+    while ($row = db_fetch_array($result)) {
+      $rfid = $row['rfid'];
+      $fees[] = uc_recurring_fee_load('user', $rfid);
+      $success = TRUE;
+    }
+
+    foreach($fees as $fee) {
+      $new_fee = uc_recurring_renew($fee);
+
+      if ($new_order_id = $new_fee->order_id) {
+        // enter the payment.
+        $message = t('Authorize.net ARB Subscription !subscription: !amount recurring fee reported processed', array('!amount' => $_POST['x_amount']));
+        $data = array('module' => 'uc_authorizenet', 'subscription id' => $_POST['x_subscription_id']);
+        uc_payment_enter($new_order_id, 'credit', $_POST['x_amount'], 0, $data, $message);
+
+        // Log the ARB payment if enabled.
+        if (variable_get('uc_authnet_report_arb_post', FALSE)) {
+          watchdog('uc_authorizenet', 'ARB payment reported for order @order_id: <pre>@post</pre>', array('@order_id' => $new_order_id, '@post' => print_r($_POST, TRUE)));
+        }
+      }
+      else {
+        $success = FALSE;
+      }
+    }
+
+  }
+
+  if (!$success) {
+    watchdog('uc_authorizenet', 'Error tying incoming silent post to existing order.<br/><pre>!data</pre>', array('!data' => serialize($data)), WATCHDOG_WARNING);
+  }
+
+  return $success;
 }
 
+
 /**
  * Sends an XML API Request to Authorize.Net.
  *
@@ -703,17 +809,28 @@ function uc_authorizenet_xml_api($server
  *
  * @param $order
  *   The order object containing billing and shipping information.
- * @param $fee
- *   An array of data describing the recurring fee.
+ * @param $amount
+ *  Fee charged each interval
+ * @param $first_charge_on
+ *   When first charge will happen, e.g, '2 weeks'
+ * @param $recurring_every
+ *   The interval to charge at, e.g. '1 month'
+ * @param $max_occurances
+ *   Maximum number of intervals for charge to occur
+ * @param $object
+ *   This object will be passed as an argument to hook_authorizenet_arb
+ *   just before initiating the request over the wire.  uc_recurring
+ *   passes its $fee object along this way and then fills in the blanks
+ *   later
  * @return
  *   TRUE or FALSE indicating the success of the request.
  */
-function uc_authorizenet_arb_create($order, $fee) {
+function uc_authorizenet_arb_create($order, $amount, $first_charge_on, $recurring_every, $max_occurances, $object=NULL) {
   $server = variable_get('uc_authnet_arb_mode', 'disabled');
 
   // Setup variables for the payment schedule.
-  list($length, $unit) = explode(' ', $fee->regular_interval);
-  list($trial_length, $trial_unit) = explode(' ', $fee->initial_charge);
+  list($length, $unit) = explode(' ', $recurring_every);
+  list($trial_length, $trial_unit) = explode(' ', $first_charge_on);
 
   // Convert weeks and years to days.
   if ($unit == 'weeks') {
@@ -725,21 +842,13 @@ function uc_authorizenet_arb_create($ord
     $unit = 'days';
   }
 
-  // Get a default SKU if none was supplied.
-  if (empty($fee->model)) {
-    $fee->model = db_result(db_query("SELECT model FROM {uc_products} WHERE nid = %d", $fee->nid));
-  }
-
-  // Make sure we have valid values for Authorize.Net.
-  if ($length <= 0 || $unit == 'days' && $length > 365 || $unit == 'months' && $length > 12) {
-    watchdog('uc_authorizenet', 'Product @sku has invalid interval settings for Authorize.Net - @length @unit', array('@sku' => $fee->model, '@length' => $length, '@unit' => $unit), WATCHDOG_ERROR);
-    return FALSE;
-  }
-
   // Get the country data for the billing and shipping information.
   $billing_country = uc_get_country_data(array('country_id' => $order->billing_country));
   $delivery_country = uc_get_country_data(array('country_id' => $order->delivery_country));
 
+  foreach ($order->products as $product) {
+
+  }
   // Build the data array for the request.
   $data = array(
     'refId' => substr($order->order_id .'-'. time(), 0, 20),
@@ -750,16 +859,16 @@ function uc_authorizenet_arb_create($ord
           'length' => $length,
           'unit' => $unit,
         ),
-        'startDate' => date('Y-m-d', strtotime('+ '. $fee->initial_charge)),
-        'totalOccurrences' => $fee->number_intervals,
-        'trialOccurrences' => '0',
+        'startDate' => date('Y-m-d', strtotime('+ '. $first_charge_on)),
+        'totalOccurrences' => $max_occurances,
+        'trialOccurrences' => '0', // @todo this should be implemented somehow, its nice to have
       ),
-      'amount' => round($fee->fee_amount, 2),
-      'trialAmount' => 0,
+      'amount' => round($amount, 2),
+      'trialAmount' => 0, // @todo this should be implemented somehow, its nice to have
       'payment' => array(), // Data inserted below based on payment method.
       'order' => array(
         'invoiceNumber' => substr($order->order_id, 0, 20),
-        'description' => substr(t('Order @order_id - @sku', array('@order_id' => $order->order_id, '@sku' => $fee->model)), 0, 255),
+        'description' => substr(t('Order @order_id - @created', array('@order_id' => $order->order_id, '@created' => format_date($order->created, 'small'))), 0, 255),
       ),
       'customer' => array(
         'id' => substr($order->uid, 0, 20),
@@ -790,6 +899,7 @@ function uc_authorizenet_arb_create($ord
     ),
   );
 
+
   // Strip out the shipping info if it isn't necessary.
   if (empty($data['subscription']['shipTo']['firstName'])) {
     unset($data['subscription']['shipTo']);
@@ -809,6 +919,24 @@ function uc_authorizenet_arb_create($ord
     );
   }
 
+  // Make sure we have valid values for Authorize.Net.
+  if ($length <= 0 || ($unit == 'days' && $length >= 365) || ($unit == 'months' && $length >= 12)) {
+    watchdog('uc_authorizenet', 'Order !order has invalid interval settings for Authorize.Net - @length @unit', array('!order' => $order->order_id, '@length' => $length, '@unit' => $unit), WATCHDOG_ERROR);
+    return FALSE;
+  }
+
+  // Allow modules to alter the data structure before its executed
+  // Note that the $object parameter is passed thru here
+  foreach (module_implements('authorizenet_arb') as $module) {
+    $func = $module .'_authorizenet_arb';
+    if($func('create', $data, $object) === FALSE) {
+      // Hook calls can return FALSE and it will abort this op
+      return FALSE;
+    }
+  }
+
+  $pdata = $data;
+
   // Build the XML string.
   $xml = _uc_authorizenet_xml_api_wrapper('ARBCreateSubscriptionRequest', _uc_authorizenet_array_to_xml($data));
 
@@ -827,32 +955,37 @@ function uc_authorizenet_arb_create($ord
     uc_order_comment_save($order->order_id, 0, t('Authorize.Net: Recurring fee for @model failed.<br />@error - @text', array('@model' => $fee->model, '@error' => $data['code'], '@text' => $data['text'])), 'admin');
     return FALSE;
   }
+  else {
+    return $data;
+  }
+}
 
-  $user_fee = array(
-    'rfid' => 0,
-    'uid' => $order->uid,
-    'fee_handler' => 'uc_authorizenet',
-    'next_charge' => strtotime('+'. $fee->initial_charge),
-    'fee_amount' => $fee->fee_amount,
-    'regular_interval' => $fee->regular_interval,
-    'remaining_intervals' => $fee->number_intervals,
-    'charged_intervals' => 0,
-    'order_id' => $order->order_id,
-    'data' => $data['subscriptionId'],
-  );
+/**
+ * Callback, a recurring fee has been created
+ */
+function uc_authorizenet_arb_create_after($order, $fee_id, $arb_subscription_id, &$object) {
+  $return = FALSE;
 
-  uc_recurring_fee_save('user', $user_fee);
+  // linking ARB Subscription ID to our new recurring fee
+  if ($fee_id && $arb_subscription_id) {
+    $return = array();
 
-  uc_order_comment_save($order->order_id, 0, t('Authorize.Net: Recurring fee setup for @model.<br />Subscription ID: @subscription_id', array('@model' => $fee->model, '@subscription_id' => $data['subscriptionId'])), 'admin');
+    $data = array('rfid' => $fee_id, 'arb_subscription_id' => $arb_subscription_id);
+    $result = drupal_write_record('uc_authorizenet_arb', $data);
 
-  return TRUE;
+    $authnet_url = 'https://account.authorize.net/UI/themes/anet/ARB/SubscriptionDetail.aspx?SubscrID='. $arb_subscription_id;
+    $return['order_comment_extra'] = t('Authorize.net ARB Subscription !subscriptionId', array('!subscriptionId' => '<a href="'. $authnet_url .'" target="_blank">'. $arb_subscription_id .'</a>'));
+  }
+
+  return $return;
 }
 
+
 /**
  * Updates an ARB subscription; for simplicity's sake, payment schedule
  *   information cannot be updated at this time.
  *
- * @param $subscription_id
+ * @param $arb_subscription_id
  *   The ID of the subscription at Authorize.Net.
  * @param $updates
  *   An array of data to update using key/value pairs from the XML API for ARB;
@@ -861,15 +994,15 @@ function uc_authorizenet_arb_create($ord
  * @return
  *   TRUE or FALSE indicating the success of the cancellation.
  */
-function uc_authorizenet_arb_update($subscription_id, $updates, $order_id = NULL) {
+function uc_authorizenet_arb_update($arb_subscription_id, $updates, $order_id = NULL) {
   $server = variable_get('uc_authnet_arb_mode', 'disabled');
 
   unset($updates['paymentSchedule']);
 
   // Build the data array for the request.
   $data = array(
-    'refId' => substr($order->order_id .'-'. time(), 0, 20),
-    'subscriptionId' => $subscription_id,
+    'refId' => substr($order_id .'-'. time(), 0, 20),
+    'subscriptionId' => $arb_subscription_id,
     'subscription' => $updates
   );
 
@@ -886,17 +1019,18 @@ function uc_authorizenet_arb_update($sub
 
   // Parse the response into a data array.
   $data = _uc_authorizenet_arb_parse_response($response);
-
+  drupal_set_message(kpr($data, true));
   if ($data['resultCode'] == 'Error') {
     if (!empty($order_id)) {
-      uc_order_comment_save($order_id, 0, t('Authorize.Net: Subscription @subscription_id updated failed.<br />@error - @text', array('@subscription_id' => $subscription_id, '@error' => $data['code'], '@text' => $data['text'])), 'admin');
+      uc_order_comment_save($order_id, 0, t('Authorize.Net: Subscription @subscription_id updated failed.<br />@error - @text', array('@subscription_id' => $arb_subscription_id, '@error' => $data['code'], '@text' => $data['text'])), 'admin');
     }
     return FALSE;
   }
+  else {
+    uc_order_comment_save($order_id, 0, t('Authorize.Net: Subscription @subscription_id updated.', array('@subscription_id' => $arb_subscription_id)), 'admin');
 
-  uc_order_comment_save($order_id, 0, t('Authorize.Net: Subscription @subscription_id updated.', array('@subscription_id' => $subscription_id)), 'admin');
-
-  return TRUE;
+    return $data;
+  }
 }
 
 /**
@@ -911,13 +1045,19 @@ function uc_authorizenet_arb_update($sub
  * @return
  *   TRUE or FALSE indicating the success of the cancellation.
  */
-function uc_authorizenet_arb_cancel($subscription_id, $order_id = NULL, $fee = array()) {
+function uc_authorizenet_arb_cancel($arb_subscription_id, $order_id = NULL) {
+  static $last_rfid;
+
+  // This may be invoked twice for a single fee due to the safety catch in uc_recurring_hook_fee
+  if ($last_id == $arb_subscription_id) { return TRUE; }
+  $last_id = $arb_subscription_id;
+
   $server = variable_get('uc_authnet_arb_mode', 'disabled');
 
   // Build the data array for the request.
   $data = array(
-    'refId' => substr($order->order_id .'-'. time(), 0, 20),
-    'subscriptionId' => $subscription_id,
+    'refId' => substr($order_id .'-'. time(), 0, 20),
+    'subscriptionId' => $arb_subscription_id,
   );
 
   // Build the XML string.
@@ -931,22 +1071,19 @@ function uc_authorizenet_arb_cancel($sub
     return FALSE;
   }
 
+  //drupal_set_message(kpr(array($data,$xml,$response),true));
+
   // Parse the response into a data array.
   $data = _uc_authorizenet_arb_parse_response($response);
 
   if ($data['resultCode'] == 'Error') {
     if (!empty($order_id)) {
-      uc_order_comment_save($order_id, 0, t('Authorize.Net: Subscription @subscription_id cancellation failed.<br />@error - @text', array('@subscription_id' => $subscription_id, '@error' => $data['code'], '@text' => $data['text'])), 'admin');
+      uc_order_comment_save($order_id, 0, t('Authorize.Net: Subscription @subscription_id cancellation failed.<br />@error - @text', array('@subscription_id' => $arb_subscription_id, '@error' => $data['code'], '@text' => $data['text'])), 'admin');
     }
     return FALSE;
   }
 
-  uc_order_comment_save($order_id, 0, t('Authorize.Net: Subscription @subscription_id cancelled.', array('@subscription_id' => $subscription_id)), 'admin');
-
-  // Let other modules act on the canceled fee.
-  if (!empty($fee)) {
-    module_invoke_all('uc_arb_cancel', $fee);
-  }
+  uc_order_comment_save($order_id, 0, t('Authorize.Net: Subscription @subscription_id cancelled.', array('@subscription_id' => $arb_subscription_id)), 'admin');
 
   return TRUE;
 }
Index: payment/uc_authorizenet/uc_authorizenet.pages.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ubercart/payment/uc_authorizenet/Attic/uc_authorizenet.pages.inc,v
retrieving revision 1.1.2.2
diff -u -p -r1.1.2.2 uc_authorizenet.pages.inc
--- payment/uc_authorizenet/uc_authorizenet.pages.inc	28 Apr 2009 21:03:14 -0000	1.1.2.2
+++ payment/uc_authorizenet/uc_authorizenet.pages.inc	29 Jun 2009 02:33:27 -0000
@@ -11,16 +11,17 @@
 // Receives a payment notification and handles it appropriately.
 function uc_authorizenet_silent_post() {
   if (variable_get('uc_authnet_report_arb_post', FALSE)) {
-    watchdog('uc_authorizenet', '!arbSilent POST received: <pre>@post</pre>', array('!arb' => (isset($_POST['x_subscription_id']) && isset($_POST['x_subscription_paynum'])) ? 'ARB ' : '', '@post' => print_r($_POST, TRUE)));
+    //watchdog('uc_authorizenet', '!arbSilent POST received: <pre>@post</pre>', array('!arb' => (isset($_POST['x_subscription_id']) && isset($_POST['x_subscription_paynum'])) ? 'ARB ' : '', '@post' => print_r($_POST, TRUE)));
   }
 
-  // Decrypt the Auth.Net API login data.
-  $login_data = _uc_authorizenet_login_data();
+  //
+  // If we're receiving notification for an ARB payment...
+  if ($_POST['x_subscription_id'] && $_POST['x_subscription_paynum']) {
+    // Decrypt the Auth.Net API login data.
+    $login_data = _uc_authorizenet_login_data();
 
-  // TODO: Modify the MD5 hash to accommodate differences from AIM to ARB.
+    // TODO: Modify the MD5 hash to accommodate differences from AIM to ARB.
 
-  // If we're receiving notification for an ARB payment...
-  if (isset($_POST['x_subscription_id']) && isset($_POST['x_subscription_paynum'])) {
     // Compare our expected MD5 Hash against what was received.
     $md5 = strtoupper(md5($login_data['md5_hash'] . $_POST['x_trans_id'] . $_POST['x_amount']));
 
@@ -29,29 +30,7 @@ function uc_authorizenet_silent_post() {
       watchdog('uc_authorizenet', 'Invalid ARB payment notification received.', array(), WATCHDOG_ERROR);
     }
     else {
-      // Otherwise, update the recurring fee information in our database.
-      $fee = db_fetch_array(db_query("SELECT * FROM {uc_recurring_users} WHERE fee_handler = 'uc_authorizenet' AND data = '%s'", $_POST['x_subscription_id']));
-
-      // Only process if the fee actually exists!
-      if (!empty($fee)) {
-        // Update the interval counters.
-        $fee['remaining_intervals'] -= 1;
-        $fee['charged_intervals'] += 1;
-
-        // Set the next expected charge time.
-        $fee['next_charge'] = strtotime('+'. $fee['regular_interval'], $fee['next_charge']);
-
-        // Save the new values.
-        uc_recurring_fee_save('user', $fee);
-
-        // Log the ARB payment if enabled.
-        if (variable_get('uc_authnet_report_arb_post', FALSE)) {
-          watchdog('uc_authorizenet', 'ARB payment reported for order @order_id: <pre>@post</pre>', array('@order_id' => $fee['order_id'], '@post' => print_r($_POST, TRUE)));
-        }
-
-        // Let other modules act on the data.
-        module_invoke_all('uc_arb_payment', $fee);
-      }
+      module_invoke_all('authorizenet_silent_post', $_POST);
     }
   }
 
@@ -73,7 +52,7 @@ function uc_authorizenet_arb_user_update
     '#value' => $rfid,
   );
   $form['description'] = array(
-    '#value' => '<div>'. t('Recurring fee order ID: @order_id', array('@order_id' => $fee['order_id'])) .'</div>',
+    '#value' => '<div>'. t('Recurring fee order ID: @order_id', array('@order_id' => $fee->order_id)) .'</div>',
   );
 
   $form['cc_data'] = array(
@@ -96,6 +75,7 @@ function uc_authorizenet_arb_user_update
 
 function uc_authorizenet_arb_user_update_form_submit($form, &$form_state) {
   $fee = uc_recurring_fee_load('user', $form_state['values']['rfid']);
+  $subscription_id = $fee->data['subscriptionId'];
 
   $updates = array(
     'payment' => array(
@@ -106,7 +86,7 @@ function uc_authorizenet_arb_user_update
     ),
   );
 
-  $result = uc_authorizenet_arb_update($fee['data'], $updates, $fee['order_id']);
+  $result = uc_authorizenet_arb_update($subscription_id, $updates, $fee->order_id);
 
   // If the update was successful...
   if ($result) {
@@ -135,15 +115,16 @@ function uc_authorizenet_arb_user_cancel
 
 function uc_authorizenet_arb_user_cancel_form_submit($form, &$form_state) {
   $fee = uc_recurring_fee_load('user', $form_state['values']['rfid']);
+  $subscription_id = $fee->data['subscriptionId'];
 
-  $result = uc_authorizenet_arb_cancel($fee['data'], $fee['order_id'], $fee);
+  $result = uc_authorizenet_arb_cancel($subscription_id, $fee->order_id, $fee);
 
   // If the cancellation was successful...
   if ($result) {
     drupal_set_message(t('The recurring fee has been canceled.'));
 
     // Set the fee's recurring charges to 0.
-    uc_recurring_fee_cancel($fee['rfid']);
+    uc_recurring_fee_cancel($fee->rfid);
   }
   else {
     drupal_set_message(t('An error has occurred. Please try again and contact us if the problem persists.'), 'error');
Index: payment/uc_credit/test_gateway.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ubercart/payment/uc_credit/test_gateway.module,v
retrieving revision 1.1.2.6
diff -u -p -r1.1.2.6 test_gateway.module
--- payment/uc_credit/test_gateway.module	18 Apr 2009 07:29:17 -0000	1.1.2.6
+++ payment/uc_credit/test_gateway.module	29 Jun 2009 02:33:27 -0000
@@ -67,3 +67,97 @@ function test_gateway_charge($order_id, 
 
   return $result;
 }
+
+/**
+ * Callback to process a recurring renewal
+ */
+function uc_recurring_test_gateway_renew($order, $fee) {
+  if ($key = uc_credit_encryption_key()) {
+    $crypt = new uc_encryption_class;
+    $fee['data']['payment_details']['cc_number'] = $crypt->decrypt($key, $fee['data']['payment_details']['cc_number']);
+    if (variable_get('uc_credit_debug', FALSE)) {
+      $fee['data']['payment_details']['cc_cvv'] = $crypt->decrypt($key, $fee['data']['payment_details']['cc_cvv']);
+    }
+    $fee['data']['payment_details']['cc_exp_month'] = $crypt->decrypt($key, $fee['data']['payment_details']['cc_exp_month']);
+    $fee['data']['payment_details']['cc_exp_year'] = $crypt->decrypt($key, $fee['data']['payment_details']['cc_exp_year']);
+    uc_store_encryption_errors($crypt, 'uc_recurring');
+  }
+
+  // Cache the CC details stored by the handler.
+  uc_credit_cache('save', $fee['data']['payment_details'], FALSE);
+
+  // Run the charge.
+  $result = test_gateway_charge($fee['order_id'], $fee['fee_amount'], NULL);
+
+  // Handle the result.
+  if ($result['success'] === TRUE) {
+    uc_payment_enter($fee['order_id'], 'credit', $fee['fee_amount'], 0, $result['data'], t('Recurring fee payment.') .'<br />'. $result['comment']);
+  }
+  else {
+  }
+
+  return $result['success'];
+}
+
+/**
+ * Callback function for setting up a recurring fee
+ */
+function uc_recurring_test_gateway_fee($order, &$fee) {
+  $data = array(
+    'billing_first_name' => $order->billing_first_name,
+    'billing_last_name' => $order->billing_last_name,
+    'billing_phone' => $order->billing_phone,
+    'billing_company' => $order->billing_company,
+    'billing_street1' => $order->billing_street1,
+    'billing_street2' => $order->billing_street2,
+    'billing_city' => $order->billing_city,
+    'billing_zone' => $order->billing_zone,
+    'billing_postal_code' => $order->billing_postal_code,
+    'billing_country' => $order->billing_country,
+    'payment_details' => $order->payment_details,
+    'model' => $fee->model,
+  );
+
+  if ($key = uc_credit_encryption_key()) {
+    $crypt = new uc_encryption_class;
+    $data['payment_details']['cc_number'] = $crypt->encrypt($key, $data['payment_details']['cc_number'], 32);
+    if (variable_get('uc_credit_debug', FALSE)) {
+      $data['payment_details']['cc_cvv'] = $crypt->encrypt($key, $data['payment_details']['cc_cvv'], 32);
+    }
+    $data['payment_details']['cc_exp_month'] = $crypt->encrypt($key, $data['payment_details']['cc_exp_month'], 32);
+    $data['payment_details']['cc_exp_year'] = $crypt->encrypt($key, $data['payment_details']['cc_exp_year'], 32);
+    uc_store_encryption_errors($crypt, 'uc_recurring');
+
+    // need to set gateway specific fee information for renewals
+    $fee->fee_handler = 'test_gateway';
+    $fee->data = $data;
+  }
+
+  return TRUE;
+}
+
+/**
+ * Callback for uc_recurring_fee_ops().
+ */
+function uc_recurring_test_gateway_fee_ops($context, $fee) {
+  $ops = array();
+
+  switch ($context) {
+    case 'fee_admin':
+      if ($fee['remaining_intervals'] > 0) {
+        $ops[] = l(t('edit'), 'admin/store/orders/recurring/'. $fee['rfid'] .'/edit');
+        $ops[] = l(t('cancel'), 'admin/store/orders/recurring/'. $fee['rfid'] .'/cancel');
+      }
+      else {
+        $ops[] = l(t('delete'), 'admin/store/orders/recurring/'. $fee['rfid'] .'/delete');
+      }
+      break;
+    case 'user':
+      $ops[] = l(t('Cancel'), 'user/'. $fee['uid'] .'/recurring/'. $fee['rfid'] .'/cancel');
+      break;
+  }
+
+  return $ops;
+}
+  
+
Index: payment/uc_credit/uc_credit.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ubercart/payment/uc_credit/uc_credit.module,v
retrieving revision 1.5.2.14
diff -u -p -r1.5.2.14 uc_credit.module
--- payment/uc_credit/uc_credit.module	2 Apr 2009 21:01:46 -0000	1.5.2.14
+++ payment/uc_credit/uc_credit.module	29 Jun 2009 02:33:28 -0000
@@ -366,7 +366,7 @@ function uc_credit_payment_method() {
   $cc_types = array('visa', 'mastercard', 'discover', 'amex');
   foreach ($cc_types as $type) {
     if (variable_get('uc_credit_'. $type, TRUE)) {
-      $title .= ' <img src="'. $path .'/images/'. $type .'.gif" style="position: relative; top: 5px;">';
+      $title .= ' <img src="'. $path .'/images/'. $type .'.gif" alt="'. $type .'" style="position: relative; top: 5px;" />';
     }
   }
   $methods[] = array(
@@ -382,7 +382,6 @@ function uc_credit_payment_method() {
   return $methods;
 }
 
-
 /*******************************************************************************
  * Callback Functions, Forms, and Tables
  ******************************************************************************/
@@ -871,6 +870,33 @@ function uc_credit_settings_form_validat
   }
 }
 
+
+/**
+ * Callback function for setting up a recurring credit card fee
+ */
+function uc_recurring_credit_fee($op, &$fee, $order) {
+  $func = 'uc_recurring_'. uc_credit_default_gateway() .'_fee';
+
+  if (function_exists($func)) {
+    return $func($op, $fee, $order);
+  }
+  // no available recurring fee handler for the default credit card gateway
+  return FALSE;
+}
+
+/**
+ * Callback function for retrieving possible operations for a recurring fee
+ */
+function uc_recurring_credit_fee_ops($context, &$fee) {
+  $handler = 'uc_recurring_'. uc_credit_default_gateway() .'_fee_ops';
+  if (function_exists($handler)) {
+    return $handler($context, $fee);
+  }
+  // no available recurring fee handler for the default credit card gateway
+  return FALSE;
+}
+
+
 // Creates the encryption key file if it doesn't already exist.
 function uc_credit_settings_form_submit($form, &$form_state) {
   if ($_SESSION['update_cc_encrypt_dir'] === TRUE) {
@@ -1089,7 +1115,7 @@ function theme_uc_payment_method_credit_
     if (variable_get('uc_credit_cvv_enabled', TRUE)) {
       $form['cc_cvv']['#title'] = '';
       $output .= '<tr><td class="field-label">'. t('CVV:') .'</td><td>'. drupal_render($form['cc_cvv'])
-                .' <img src="'. $path .'/images/info.png" onclick="cvv_info_popup();" style="cursor: pointer; position: relative; top: 3px;"> <a style="cursor: pointer; font-weight: normal;" onclick="cvv_info_popup();">'
+                .' <img src="'. $path .'/images/info.png" onclick="cvv_info_popup();" alt="" style="cursor: pointer; position: relative; top: 3px;" /> <a style="cursor: pointer; font-weight: normal;" onclick="cvv_info_popup();">'
                 . t("What's the CVV?") .'</a></td></tr>';
     }
     if (variable_get('uc_credit_bank_enabled', FALSE)) {
Index: payment/uc_payment/uc_payment.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ubercart/payment/uc_payment/uc_payment.module,v
retrieving revision 1.5.2.10
diff -u -p -r1.5.2.10 uc_payment.module
--- payment/uc_payment/uc_payment.module	18 Apr 2009 07:29:16 -0000	1.5.2.10
+++ payment/uc_payment/uc_payment.module	29 Jun 2009 02:33:29 -0000
@@ -480,6 +480,9 @@ function uc_payment_process($method, $or
     $key = array_shift(array_keys($gateways));
 
     // If we can find a callback in the gateway for the payment method...
+    // Helpful: for uc_credit and uc_authorizenet together, this would invoke uc_authorizenet_charge
+    // because $method would be 'credit', and uc_authorizenet_payment_gateway returns 'uc_authorizenet_charge'
+    // for that array key
     if (function_exists($gateways[$key][$method])) {
       // Pass the payment data onto the callback and store the result.
       $result = $gateways[$key][$method]($order_id, $amount, $data);
Index: payment/uc_payment_pack/uc_payment_pack.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ubercart/payment/uc_payment_pack/uc_payment_pack.module,v
retrieving revision 1.6.2.7
diff -u -p -r1.6.2.7 uc_payment_pack.module
--- payment/uc_payment_pack/uc_payment_pack.module	18 Apr 2009 07:29:15 -0000	1.6.2.7
+++ payment/uc_payment_pack/uc_payment_pack.module	29 Jun 2009 02:33:29 -0000
@@ -370,3 +370,56 @@ function uc_payment_method_check($op, &$
   }
 }
 
+/**
+ * Callback for setting up a manual recurring fee using check as payment method.
+ */
+function uc_recurring_check_fee($order, &$fee) {
+  $fee->fee_handler = 'manual';
+  return TRUE;
+}
+
+/**
+ * Callback for setting up a manual recurring fee using cod as payment method.
+ */
+function uc_recurring_cod_fee($order, &$fee) {
+  $fee->fee_handler = 'manual';
+  return TRUE;
+}
+
+/**
+ * Callback for setting up a manual recurring fee using other as payment method.
+ */
+function uc_recurring_other_fee($order, &$fee) {
+  $fee->fee_handler = 'manual';
+  return TRUE;
+}
+
+/**
+ * Callback for processing a renewal for recurring fee
+ */
+function uc_recurring_manual_renew($order, &$fee) {
+  return TRUE;
+}
+
+/**
+ * Callback for processing a renewal for recurring fee
+ */
+function uc_recurring_manual_fee_ops($context, $fee) {
+  $ops = array();
+  switch ($context) {
+    case 'fee_admin':
+      if ($fee['remaining_intervals'] > 0) {
+        $ops[] = l(t('edit'), 'admin/store/orders/recurring/'. $fee['rfid'] .'/edit');
+        $ops[] = l(t('cancel'), 'admin/store/orders/recurring/'. $fee['rfid'] .'/cancel');
+      }
+      else {
+        $ops[] = l(t('delete'), 'admin/store/orders/recurring/'. $fee['rfid'] .'/delete');
+      }
+      break;
+    case 'user':
+      $ops[] = l(t('Cancel'), 'user/'. $fee['uid'] .'/recurring/'. $fee['rfid'] .'/cancel');
+      break;
+  }
+
+  return $ops;
+}
Index: uc_cart/uc_cart.pages.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ubercart/uc_cart/Attic/uc_cart.pages.inc,v
retrieving revision 1.1.2.12
diff -u -p -r1.1.2.12 uc_cart.pages.inc
--- uc_cart/uc_cart.pages.inc	6 May 2009 18:38:03 -0000	1.1.2.12
+++ uc_cart/uc_cart.pages.inc	29 Jun 2009 02:33:29 -0000
@@ -439,20 +439,27 @@ function uc_cart_checkout_review_form_su
   $order = uc_order_load($_SESSION['cart_order']);
   $error = FALSE;
 
-  // Invoke it on a per-module basis instead of all at once.
-  foreach (module_implements('order') as $module) {
-    $result = module_invoke($module, 'order', 'submit', $order, NULL);
-
-    $msg_type = 'status';
-    if ($result[0]['pass'] === FALSE) {
-      $error = TRUE;
-      $msg_type = 'error';
+  // Invoke hook_order first with $op=submit and then if no problems,
+  // again with $op=approved
+  foreach (array('submit', 'approved') as $op) {
+    // Invoke it on a per-module basis instead of all at once.
+    foreach (module_implements('order') as $module) {
+      $result = module_invoke($module, 'order', $op, $order, NULL);
+
+      $msg_type = 'status';
+      if ($result[0]['pass'] === FALSE) {
+        $error = TRUE;
+        $msg_type = 'error';
+      }
+      if (!empty($result[0]['message'])) {
+        drupal_set_message($result[0]['message'], $msg_type);
+      }
+
+      // Stop invoking the hooks if there was an error.
+      if ($error) {
+        break;
+      }
     }
-    if (!empty($result[0]['message'])) {
-      drupal_set_message($result[0]['message'], $msg_type);
-    }
-
-    // Stop invoking the hooks if there was an error.
     if ($error) {
       break;
     }
Index: uc_product/uc_product.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ubercart/uc_product/uc_product.module,v
retrieving revision 1.14.2.25
diff -u -p -r1.14.2.25 uc_product.module
--- uc_product/uc_product.module	7 May 2009 20:52:29 -0000	1.14.2.25
+++ uc_product/uc_product.module	29 Jun 2009 02:33:30 -0000
@@ -1417,6 +1417,7 @@ function uc_product_add_to_cart_form_sub
  * @ingroup themeable
  */
 function theme_uc_product_price($price, $context, $options = array()) {
+  module_load_include('inc', 'uc_store', 'includes/uc_price');
   $output = '<div class="product-info '. implode(' ', (array)$context['class']) .'">';
   $output .= uc_price($price, $context, $options);
   $output .= '</div>';
