diff --git modules/uc_recurring_subscription/README.txt modules/uc_recurring_subscription/README.txt
new file mode 100644
index 0000000..1faec9a
--- /dev/null
+++ modules/uc_recurring_subscription/README.txt
@@ -0,0 +1,22 @@
+uc_recurring_subscription
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+uc_recurring_subscription is a drupal module that integrates recurring payments
+and roles and provides a set of features specifically designed for managing a
+membership/subscription website.
+
+INSTALL
+~~~~~~~
+This module requires a patch to ubercart, more detail on the issue can be found
+in the following issue:
+http://drupal.org/node/488422
+
+The patch required for this module is included in this project download under:
+module/uc_recurring_subscription/ubercart_api.patch
+
+To apply:
+1. Copy the ubercart_api.patch file to ubercart module directory.
+2. Patch by running the command:
+patch -p0 < ubercart_api.patch
+3. You shouldn't get any errors if it applies correctly.
+
diff --git modules/uc_recurring_subscription/ubercart_api.patch modules/uc_recurring_subscription/ubercart_api.patch
new file mode 100644
index 0000000..2b3d8cd
--- /dev/null
+++ modules/uc_recurring_subscription/ubercart_api.patch
@@ -0,0 +1,1514 @@
+diff --git uc_attribute/uc_attribute.module uc_attribute/uc_attribute.module
+index cf2bf33..db918f3 100644
+--- uc_attribute/uc_attribute.module
++++ uc_attribute/uc_attribute.module
+@@ -538,72 +538,524 @@ function uc_attribute_product_description($product) {
+  ******************************************************************************/
+ 
+ /**
+- * Load an attribute from the database.
++ * Load attribute objects from the database.
+  *
+- * @param $attr_id
+- *   The id of the attribute.
+- * @param $nid
+- *   If given, the attribute will have the options that have been assigned to
+- *   that $type for the attribute.
++ * @todo If we feel it necessary, we could optimize this, by inverting the
++ *  logic; that is, we could make uc_attribute load call this function and allow
++ *  this function to minimize the number of queries necessary. -cha0s
++ *
++ * @param $aids
++ *   Attribute IDs to load.
+  * @param $type
+- *   Determines whether $nid refers to a node or product class. $nid is ignored
+- *   if $type is not 'product' or 'class'.
+- * @return
+- *   An attribute object with its options.
+- */
+-function uc_attribute_load($attr_id, $nid = NULL, $type = '') {
+-  if ($nid) {
+-    switch ($type) {
+-      case 'product':
+-        $attribute = db_fetch_object(db_query("SELECT a.aid, a.name, a.label AS default_label, a.ordering AS default_ordering, a.required AS default_required, a.display AS default_display, a.description, pa.label, pa.default_option, pa.required, pa.ordering, pa.display FROM {uc_attributes} AS a LEFT JOIN {uc_product_attributes} AS pa ON a.aid = pa.aid AND pa.nid = %d WHERE a.aid = %d", $nid, $attr_id));
+-        $result = db_query("SELECT po.nid, po.oid, po.cost, po.price, po.weight, po.ordering, ao.name, ao.aid FROM {uc_product_options} AS po LEFT JOIN {uc_attribute_options} AS ao ON po.oid = ao.oid AND nid = %d WHERE aid = %d ORDER BY po.ordering, ao.name", $nid, $attr_id);
+-        break;
+-      case 'class':
+-        $attribute = db_fetch_object(db_query("SELECT a.aid, a.name, a.label AS default_label, a.ordering AS default_ordering, a.required AS default_required, a.display AS default_display, a.description, ca.default_option, ca.label, ca.required, ca.ordering, ca.display FROM {uc_attributes} AS a LEFT JOIN {uc_class_attributes} AS ca ON a.aid = ca.aid AND ca.pcid = '%s' WHERE a.aid = %d", $nid, $attr_id));
+-        $result = db_query("SELECT co.pcid, co.oid, co.cost, co.price, co.weight, co.ordering, ao.name, ao.aid FROM {uc_class_attribute_options} AS co LEFT JOIN {uc_attribute_options} AS ao ON co.oid = ao.oid AND co.pcid = '%s' WHERE ao.aid = %d ORDER BY co.ordering, ao.name", $nid, $attr_id);
+-        break;
+-      default:
+-        $attribute = db_fetch_object(db_query("SELECT * FROM {uc_attributes} WHERE aid = %d", $attr_id));
+-        $result = db_query("SELECT * FROM {uc_attribute_options} WHERE aid = %d ORDER BY ordering, name, label", $attr_id);
+-        break;
+-    }
+-    if (isset($attribute->default_ordering) && is_null($attribute->ordering)) {
+-      $attribute->ordering = $attribute->default_ordering;
+-    }
+-    if (isset($attribute->default_required) && is_null($attribute->required)) {
+-      $attribute->required = $attribute->default_required;
+-    }
+-    if (isset($attribute->default_display) && is_null($attribute->display)) {
+-      $attribute->display = $attribute->default_display;
+-    }
+-    if (isset($attribute->default_label) && is_null($attribute->label)) {
+-      $attribute->label = $attribute->default_label;
+-    }
+-    if (empty($attribute->label)) {
+-      $attribute->label = $attribute->name;
+-    }
++ *   The type of attribute. 'product', or 'class'. Any other type will fetch
++ *   a base attribute
++ * @param $id
++ *   The ID of the product/class this attribute belongs to.
++ * @return (array)
++ *   The array of loaded attributes.
++ */
++function uc_attribute_load_multiple($aids = array(), $type = '', $id = NULL) {
++  $sql = uc_attribute_type_info($type);
++  $conditions = array();
++
++  // Filter by the attribute IDs requested.
++  if (!empty($aids)) {
++    // Sanity check - filter out non-numeric attribute IDs.
++    $conditions[] = "ua.aid IN (". implode(", ", array_filter($aids, 'is_numeric')) .")";
+   }
++
++  // Product/class attributes.
++  if (!empty($type)) {
++    $conditions[] = "uca.{$sql['id']} = {$sql['placeholder']}";
++    $conditions = implode(" AND", $conditions);
++    // Seems like a big query to get attribute IDs, but it's all about the sort.
++    // (I'm not sure if the default ordering is propagating down correctly here.
++    // It appears that product/class attributes with no ordering won't let the
++    // attribute's propagate down, as it does when loading. -cha0s)
++    $result = db_query("
++      SELECT    uca.aid
++      FROM      {$sql['attr_table']} AS uca
++      LEFT JOIN {uc_attributes} AS ua ON uca.aid = ua.aid
++      WHERE     $conditions
++      ORDER BY  uca.ordering, ua.name", $id);
++  }
++
++  // Base attributes.
+   else {
+-    $attribute = db_fetch_object(db_query("SELECT * FROM {uc_attributes} WHERE aid = %d", $attr_id));
+-    $result = db_query("SELECT * FROM {uc_attribute_options} WHERE aid = %d ORDER BY ordering, name", $attr_id);
++    // Padding just to make sure that everything's fine if we don't get an aid
++    // condition. Keeps it elegant.
++    $conditions[] = "1";
++    $conditions = implode(" AND ", $conditions);
++
++    $result = db_query("SELECT aid FROM {uc_attributes} ua WHERE $conditions ORDER BY ordering, name");
++  }
++
++  // Load the attributes.
++  $attributes = array();
++  while ($aid = db_result($result)) {
++    $attributes[$aid] = uc_attribute_load($aid, $id, $type);
++  }
++
++  return $attributes;
++}
++
++/**
++ * Load an attribute from the database.
++ *
++ * @param $aid
++ *   The ID of the attribute.
++ * @param $type
++ *   The type of attribute. 'product', or 'class'. Any other type will fetch
++ *   a base attribute
++ * @param $id
++ *   The ID of the product/class this attribute belongs to.
++ * @return
++ *   The attribute object, or FALSE if it doesn't exist.
++ */
++function uc_attribute_load($aid, $id = NULL, $type = '') {
++  $sql = uc_attribute_type_info($type);
++
++  switch ($type) {
++    case 'product':
++    case 'class':
++
++      // Read attribute data.
++      $attribute = db_fetch_object(db_query("
++        SELECT    a.aid, a.name, a.label AS default_label, a.ordering AS default_ordering,
++                  a.required AS default_required, a.display AS default_display,
++                  a.description, pa.label, pa.default_option, pa.required, pa.ordering,
++                  pa.display, pa.{$sql['id']}
++        FROM      {uc_attributes} AS a
++        LEFT JOIN {$sql['attr_table']} AS pa ON a.aid = pa.aid AND
++                  pa.{$sql['id']} = {$sql['placeholder']}
++        WHERE a.aid = %d", $id, $aid));
++
++      // Don't try to build it further if it failed already.
++      if (!$attribute) return FALSE;
++
++      // Set any missing defaults.
++      foreach (array('ordering', 'required', 'display', 'label') as $field) {
++        if (isset($attribute->{"default_$field"}) && is_null($attribute->$field)) {
++          $attribute->$field = $attribute->{"default_$field"};
++        }
++      }
++      if (empty($attribute->label)) {
++        $attribute->label = $attribute->name;
++      }
++
++      // Read option data.
++      $result = db_query("
++        SELECT    po.{$sql['id']}, po.oid, po.cost, po.price, po.weight, po.ordering, ao.name,
++                  ao.aid
++        FROM      {$sql['opt_table']} AS po
++        LEFT JOIN {uc_attribute_options} AS ao ON po.oid = ao.oid AND
++                  po.{$sql['id']} = {$sql['placeholder']}
++        WHERE     aid = %d ORDER BY po.ordering, ao.name", $id, $aid);
++
++    break;
++
++    default:
++
++      // Read attribute and option data.
++      $attribute = db_fetch_object(db_query("SELECT * FROM {uc_attributes} WHERE aid = %d", $aid));
++      $result = db_query("SELECT * FROM {uc_attribute_options} WHERE aid = %d ORDER BY ordering, name", $aid);
++
++      // Don't try to build it further if it failed already.
++      if (!$attribute) return FALSE;
++
++    break;
+   }
++
++  // Got an attribute?
+   if ($attribute) {
++    // Get its options, too.
+     $attribute->options = array();
+     while ($option = db_fetch_object($result)) {
+       $attribute->options[$option->oid] = $option;
+     }
+   }
++
+   return $attribute;
+ }
+ 
+ /**
+- * Load the option identified by $oid.
++ * Fetch an array of attribute objects from the database who belong to a product.
++ *
++ * @param $nid
++ *   Product whose attributes to load.
++ * @return (array)
++ *   The array of attribute objects.
++ */
++function uc_attribute_load_product_attributes($nid) {
++  return uc_attribute_load_multiple(array(), 'product', $nid);
++}
++
++/**
++ * Save an attribute object to the database.
++ *
++ * @param $attribute
++ *   The attribute object to save.
++ * @return (integer)
++ *   Return the result from drupal_write_record().
++ */
++function uc_attribute_save(&$attribute) {
++  // Insert or update?
++  $key = empty($attribute->aid) ? NULL : 'aid';
++  return drupal_write_record('uc_attributes', $attribute, $key);
++}
++
++/**
++ * Delete an attribute from the database.
++ *
++ * @param $aid
++ *   Attribute ID to delete.
++ * @return (integer)
++ *   Return the Drupal SAVED_DELETED flag.
++ */
++function uc_attribute_delete($aid) {
++  // Delete the class attributes and their options.
++  uc_attribute_subject_delete($aid, 'class');
++
++  // Delete the product attributes and their options.
++  uc_attribute_subject_delete($aid, 'product');
++
++  // Delete base attributes and their options.
++  db_query("DELETE FROM {uc_attribute_options} WHERE aid = %d", $aid);
++  db_query("DELETE FROM {uc_attributes} WHERE aid = %d", $aid);
++
++  return SAVED_DELETED;
++}
++
++/**
++ * Load an attribute option from the database.
++ *
++ * @param $oid
++ *   Option ID to load.
++ * @return (object)
++ *   The attribute option object.
+  */
+ function uc_attribute_option_load($oid) {
+   return db_fetch_object(db_query("SELECT * FROM {uc_attribute_options} WHERE oid = %d", $oid));
+ }
+ 
+ /**
++ * Save an attribute object to the database.
++ *
++ * @param $option
++ *   The attribute option object to save.
++ * @return (integer)
++ *   Return the result from drupal_write_record().
++ */
++function uc_attribute_option_save(&$option) {
++  // Insert or update?
++  $key = empty($option->oid) ? NULL : 'oid';
++  return drupal_write_record('uc_attribute_options', $option, $key);
++}
++
++/**
++ * Delete an attribute option from the database.
++ *
++ * @param $oid
++ *   Option ID to delete.
++ * @return (integer)
++ *   Return the Drupal SAVED_DELETED flag.
++ */
++function uc_attribute_option_delete($oid) {
++  // Delete the class attribute options.
++  uc_attribute_subject_option_delete($oid, 'class');
++
++  // Delete the product attribute options. (and the adjustments!)
++  uc_attribute_subject_option_delete($oid, 'product');
++
++  // Delete base attributes and their options.
++  db_query("DELETE FROM {uc_attribute_options} WHERE oid = %d", $oid);
++
++  return SAVED_DELETED;
++}
++
++/**
++ * Save a product/class attribute.
++ *
++ * @param &$attribute
++ *   The product/class attribute.
++ * @param $type
++ *   Is this a product or a class?
++ * @param $id
++ *   The product/class ID.
++ * @param $save_options
++ *   Save the product/class attribute's options, too?
++ * @return (integer)
++ *   Return the result from drupal_write_record().
++ */
++function uc_attribute_subject_save(&$attribute, $type, $id, $save_options = FALSE) {
++  $sql = uc_attribute_type_info($type);
++
++  // Insert or update?
++  $key = uc_attribute_subject_exists($attribute->aid, $type, $id) ? array('aid', $sql['id']) : NULL;
++
++  // First, save the options. First because if this is an insert, we'll set
++  // a default option for the product/class attribute.
++  if ($save_options && is_array($attribute->options)) {
++    foreach ($attribute->options as $option) {
++      // Sanity check!
++      $option = (object) $option;
++      uc_attribute_subject_option_save($option, $type, $id);
++    }
++
++    // Is this an insert? If so, we'll set the default option.
++    if (empty($key)) {
++      $default_option = 0;
++      // Make the first option (if any) the default.
++      if (is_array($attribute->options)) {
++        $option = (object) reset($attribute->options);
++        $default_option = $option->oid;
++      }
++      $attribute->default_option = $default_option;
++    }
++  }
++
++  // Merge in the product/class attribute's ID and save.
++  $attribute->{$type == 'product' ? 'nid' : 'pcid'} = $id;
++  $result = drupal_write_record(trim($sql['attr_table'], '{}'), $attribute, $key);
++
++  return $result;
++}
++
++/**
++ * Delete all the options associated with this product/class attribute, and
++ * then the attribute itself.
++ *
++ * @param $aid
++ *   The base attribute ID.
++ * @param $type
++ *   Is this a product or a class?
++ * @param $id
++ *   The product/class ID.
++ * @return (integer)
++ *   Return the Drupal SAVED_DELETED flag.
++ */
++function uc_attribute_subject_delete($aid, $type, $id = NULL) {
++  $sql = uc_attribute_type_info($type);
++
++  // Base conditions, and an ID check if necessary.
++  $conditions[] = "aid = %d";
++  if ($id) {
++    $conditions[] = "{$sql['id']} = {$sql['placeholder']}";
++  }
++  $conditions = implode(" AND ", $conditions);
++
++  $result = db_query("SELECT a.oid FROM {uc_attribute_options} AS a JOIN {$sql['opt_table']} AS subject ON a.oid = subject.oid WHERE $conditions", $aid, $id);
++  while ($oid = db_result($result)) {
++    // Don't delete the adjustments one at a time. We'll do it in bulk soon for
++    // efficiency.
++    uc_attribute_subject_option_delete($oid, $type, $id, FALSE);
++  }
++  db_query("DELETE FROM {$sql['attr_table']} WHERE $conditions", $aid, $id);
++
++  // If this is a product attribute, wipe any associated adjustments.
++  if ($type == 'product') {
++    uc_attribute_adjustments_delete(array(
++      'aid' => $aid,
++      'nid' => $id,
++    ));
++  }
++
++  return SAVED_DELETED;
++}
++
++/**
++ * Load a product/class attribute option.
++ *
++ * @param $oid
++ *   The product/class attribute option ID.
++ * @param $type
++ *   Is this a product or a class?
++ * @param $id
++ *   The product/class ID.
++ * @return (object)
++ *   Return the product/class attribute option.
++ */
++function uc_attribute_subject_option_load($oid, $type, $id) {
++  $sql = uc_attribute_type_info($type);
++
++  $result = db_query("
++    SELECT    po.{$sql['id']}, po.oid, po.cost, po.price, po.weight, po.ordering, ao.name,
++              ao.aid
++    FROM      {$sql['opt_table']} AS po
++    LEFT JOIN {uc_attribute_options} AS ao ON po.oid = ao.oid AND
++              po.{$sql['id']} = {$sql['placeholder']}
++    WHERE     po.oid = %d ORDER BY po.ordering, ao.name", $id, $oid);
++
++  return db_fetch_object($result);
++}
++
++/**
++ * Save a product/class attribute option.
++ *
++ /
++ * @param &$option
++ *   The product/class attribute option.
++ * @param $type
++ *   Is this a product or a class?
++ * @param $id
++ *   The product/class ID.
++ * @return (integer)
++ *   Return the result from drupal_write_record().
++ */
++function uc_attribute_subject_option_save(&$option, $type, $id) {
++  $sql = uc_attribute_type_info($type);
++
++  // Insert or update?
++  $key = uc_attribute_subject_option_exists($option->oid, $type, $id) ? array('oid', $sql['id']) : NULL;
++
++  // Merge in the product/class attribute option's ID, and save.
++  $option->{$type == 'product' ? 'nid' : 'pcid'} = $id;
++  $result = drupal_write_record(trim($sql['opt_table'], '{}'), $option, $key);
++
++  return $result;
++}
++
++/**
++ * Delete a product/class attribute option.
++ *
++ * @param $oid
++ *   The base attribute's option ID.
++ * @param $type
++ *   Is this a product or a class?
++ * @param $id
++ *   The product/class ID.
++ * @return (integer)
++ *   Return the Drupal SAVED_DELETED flag.
++ */
++function uc_attribute_subject_option_delete($oid, $type, $id = NULL, $adjustments = TRUE) {
++  $sql = uc_attribute_type_info($type);
++
++  // Base conditions, and an ID check if necessary.
++  $conditions[] = "oid = %d";
++  if ($id) {
++    $conditions[] = "{$sql['id']} = {$sql['placeholder']}";
++  }
++  $conditions = implode(" AND ", $conditions);
++
++  // Delete the option.
++  db_query("DELETE FROM {$sql['opt_table']} WHERE $conditions", $oid, $id);
++
++  // If this is a product, clean up the associated adjustments.
++  if ($adjustments && $type == 'product') {
++    uc_attribute_adjustments_delete(array(
++      'aid' => uc_attribute_option_load($oid)->aid,
++      'oid' => $oid,
++      'nid' => $id,
++    ));
++  }
++
++  return SAVED_DELETED;
++}
++
++/**
++ * @param $fields
++ *   Fields used to build a condition to delete adjustments against. Fields
++ *   currently handled are 'aid', 'oid', and 'nid'.
++ * @return (integer)
++ *   Return the Drupal SAVED_DELETED flag.
++ */
++function uc_attribute_adjustments_delete($fields) {
++
++  // Build the serialized string to match against adjustments.
++  $match = '';
++  if (!empty($fields['aid'])) {
++    $match .= serialize((integer) $fields['aid']);
++  }
++  if (!empty($fields['oid'])) {
++    $match .= serialize((string) $fields['oid']);
++  }
++
++  // Assemble the conditions and args for the SQL.
++  $args = $conditions = array();
++
++  // If we have to match aid or oid...
++  if ($match) {
++    $conditions[] = "combination LIKE '%%%s%%'";
++    $args[] = $match;
++  }
++
++  // If we've got a node ID to match.
++  if (!empty($fields['nid'])) {
++    $conditions[] = "nid = %d";
++    $args[] = $fields['nid'];
++  }
++  $conditions = implode(" AND ", $conditions);
++
++  // Delete what's necessary,
++  if ($conditions) {
++    db_query("DELETE FROM {uc_product_adjustments} WHERE $conditions", $args);
++  }
++
++  return SAVED_DELETED;
++}
++
++/**
++ * Check if a product/class attribute exists.
++ *
++ * @param $aid
++ *   The base attribute ID.
++ * @param $id
++ *   The product/class attribute's ID.
++ * @param $type
++ *   Is this a product or a class?
++ * @return (bool)
++ */
++function uc_attribute_subject_exists($aid, $type, $id) {
++  $sql = uc_attribute_type_info($type);
++  return FALSE !== db_result(db_query("SELECT aid FROM {$sql['attr_table']} WHERE aid = %d AND {$sql['id']} = {$sql['placeholder']}", $aid, $id));
++}
++
++/**
++ * Check if a product/class attribute option exists.
++ *
++ * @param $oid
++ *   The base attribute option ID.
++ * @param $id
++ *   The product/class attribute option's ID.
++ * @param $type
++ *   Is this a product or a class?
++ * @return (bool)
++ */
++function uc_attribute_subject_option_exists($oid, $type, $id) {
++  $sql = uc_attribute_type_info($type);
++  return FALSE !== db_result(db_query("SELECT oid FROM {$sql['opt_table']} WHERE oid = %d AND {$sql['id']} = {$sql['placeholder']}", $oid, $id));
++}
++
++/**
++ * Return a list of db helpers to abstract the queries between products/classes.
++ * @param $type
++ *   Is this a product or a class?
++ * @return (array)
++ *   Information helpful for creating SQL queries dealing with attributes.
++ */
++function uc_attribute_type_info($type) {
++  switch ($type) {
++    case 'product':
++      return array(
++        'attr_table' => '{uc_product_attributes}',
++        'opt_table' => '{uc_product_options}',
++        'id' => 'nid',
++        'placeholder' => '%d',
++      );
++    break;
++
++    case 'class':
++      return array(
++        'attr_table' => '{uc_class_attributes}',
++        'opt_table' => '{uc_class_attribute_options}',
++        'id' => 'pcid',
++        'placeholder' => "'%s'",
++      );
++    break;
++  }
++}
++
++/**
+  * Load all attributes associated with a product node.
+  */
+ function uc_product_get_attributes($nid) {
+diff --git uc_attribute/uc_attribute.test uc_attribute/uc_attribute.test
+new file mode 100644
+index 0000000..60565bf
+--- /dev/null
++++ uc_attribute/uc_attribute.test
+@@ -0,0 +1,688 @@
++<?php
++// $Id$
++
++/**
++ * @file
++ * Ubercart Attribute Tests
++ */
++
++class UbercartAttributeTestCase extends DrupalWebTestCase {
++
++  function getInfo() {
++    return array(
++      'name' => t('Attribute API'),
++      'description' => t('Test the attribute API.'),
++      'group' => t('Ubercart'),
++    );
++  }
++
++  function setUp() {
++    parent::setUp('token', 'uc_store', 'uc_product', 'uc_attribute', 'ca', 'uc_order', 'uc_cart');
++
++    $admin_user = $this->drupalCreateUser(array('administer store', 'administer attributes', 'administer products', 'administer product classes'));
++    $this->drupalLogin($admin_user);
++  }
++
++  public function testAttributeAPI() {
++
++    // Create an attribute.
++    $attribute = self::createAttribute();
++
++    // Test retrieval.
++    $loaded_attribute = uc_attribute_load($attribute->aid);
++
++    // Check the attribute integrity.
++    foreach (self::_attributeFieldsToTest() as $field) {
++      if ($loaded_attribute->$field != $attribute->$field) {
++        $this->fail(t('Attribute integrity check failed.'), t('Ubercart'));
++        break;
++      }
++    }
++
++    // Add a product.
++    $product = UbercartProductTestCase::createProduct();
++
++    // Attach the attribute to a product.
++    uc_attribute_subject_save($attribute, 'product', $product->nid);
++
++    // Confirm the database is correct.
++    $this->assertEqual(
++      $attribute->aid,
++      db_result(db_query("SELECT aid FROM {uc_product_attributes} WHERE nid = %d", $product->nid)),
++      t('Attribute was attached to a product properly.'),
++      t('Ubercart')
++    );
++    $this->assertTrue(uc_attribute_subject_exists($attribute->aid, 'product', $product->nid));
++
++    // Test retrieval.
++    $loaded_attribute = uc_attribute_load($attribute->aid, 'product', $product->nid);
++
++    // Check the attribute integrity.
++    foreach (self::_attributeFieldsToTest('product') as $field) {
++      if ($loaded_attribute->$field != $attribute->$field) {
++        $this->fail(t('Attribute integrity check failed.'), t('Ubercart'));
++        break;
++      }
++    }
++
++    // Delete it.
++    uc_attribute_subject_delete($attribute->aid, 'product', $product->nid);
++
++    // Confirm again.
++    $this->assertFalse(
++      db_result(db_query("SELECT aid FROM {uc_product_attributes} WHERE nid = %d", $product->nid)),
++      t('Attribute was detached from a product properly.'),
++      t('Ubercart')
++    );
++    $this->assertFalse(uc_attribute_subject_exists($attribute->aid, 'product', $product->nid));
++
++    // Add a product class.
++    $product_class = UbercartProductTestCase::createProductClass();
++
++    // Attach the attribute to a product class.
++    uc_attribute_subject_save($attribute, 'class', $product_class->pcid);
++
++    // Confirm the database is correct.
++    $this->assertEqual(
++      $attribute->aid,
++      db_result(db_query("SELECT aid FROM {uc_class_attributes} WHERE pcid = '%s'", $product_class->pcid)),
++      t('Attribute was attached to a product class properly.'),
++      t('Ubercart')
++    );
++    $this->assertTrue(uc_attribute_subject_exists($attribute->aid, 'class', $product_class->pcid));
++
++    // Test retrieval.
++    $loaded_attribute = uc_attribute_load($attribute->aid, 'class', $product_class->pcid);
++
++    // Check the attribute integrity.
++    foreach (self::_attributeFieldsToTest('class') as $field) {
++      if ($loaded_attribute->$field != $attribute->$field) {
++        $this->fail(t('Attribute integrity check failed.'), t('Ubercart'));
++        break;
++      }
++    }
++
++    // Delete it.
++    uc_attribute_subject_delete($attribute->aid, 'class', $product_class->pcid);
++
++    // Confirm again.
++    $this->assertFalse(
++      db_result(db_query("SELECT aid FROM {uc_class_attributes} WHERE pcid = '%s'", $product_class->pcid)),
++      t('Attribute was detached from a product class properly.'),
++      t('Ubercart')
++    );
++    $this->assertFalse(uc_attribute_subject_exists($attribute->aid, 'class', $product_class->pcid));
++
++    // Create a few more.
++    for ($i = 0; $i < 5; $i++) {
++      $a = self::createAttribute();
++      $attributes[$a->aid] = $a;
++    }
++
++    // Add some options, organizing them by aid and oid.
++    $attribute_aids = array_keys($attributes);
++
++    $all_options = array();
++    foreach ($attribute_aids as $aid) {
++      for ($i = 0; $i < 3; $i++) {
++        $option = self::createAttributeOption(array('aid' => $aid));
++        $all_options[$option->aid][$option->oid] = $option;
++      }
++    }
++    for ($i = 0; $i < 3; $i++) {
++      $option = self::createAttributeOption(array('aid' => $attribute->aid));
++      $all_options[$option->aid][$option->oid] = $option;
++    }
++
++    // Get the options.
++    $attribute = uc_attribute_load($attribute->aid);
++
++    // Load every attribute we got.
++    $attributes_with_options = uc_attribute_load_multiple();
++
++    // Make sure all the new options are on attributes correctly.
++    foreach ($all_options as $aid => $options) {
++      foreach ($options as $oid => $option) {
++        foreach (self::_attributeOptionFieldsToTest() as $field) {
++          if ($option->$field != $attributes_with_options[$aid]->options[$oid]->$field) {
++            $this->fail(t('Option integrity check failed.'), t('Ubercart'));
++            break;
++          }
++        }
++      }
++    }
++
++    // Pick 5 keys to check at random.
++    $aids = drupal_map_assoc(array_rand($attributes, 3));
++
++    // Load the attributes back.
++    $loaded_attributes = uc_attribute_load_multiple($aids);
++
++    // Make sure we only got the attributes we asked for. No more, no less.
++    $this->assertEqual(count($aids), count($loaded_attributes), t('Verifying attribute result.'), t('Ubercart'));
++    $this->assertEqual(count($aids), count(array_intersect_key($aids, $loaded_attributes)), t('Verifying attribute result.'), t('Ubercart'));
++
++    // Check the attributes' integrity.
++    foreach ($loaded_attributes as $aid => $loaded_attribute) {
++      foreach (self::_attributeFieldsToTest() as $field) {
++        if ($attributes[$aid]->$field != $loaded_attributes[$aid]->$field) {
++          $this->fail(t('Attribute integrity check failed.'), t('Ubercart'));
++          break;
++        }
++      }
++    }
++
++    // Add the selected attributes to the product.
++    foreach ($loaded_attributes as $loaded_attribute) {
++      uc_attribute_subject_save($loaded_attribute, 'product', $product->nid, TRUE);
++    }
++
++    // Test loading all product attributes. (This covers uc_attribute_load_product_attributes(),
++    // as the semantics are the same -cha0s)
++    $loaded_product_attributes = uc_attribute_load_multiple(array(), 'product', $product->nid);
++
++    // We'll get all in $loaded_attributes above, plus the original.
++    $product_attributes = $loaded_attributes;
++
++    // Make sure we only got the attributes we asked for. No more, no less.
++    $this->assertEqual(count($loaded_product_attributes), count($product_attributes), t('Verifying attribute result.'), t('Ubercart'));
++    $this->assertEqual(count($loaded_product_attributes), count(array_intersect_key($loaded_product_attributes, $product_attributes)), t('Verifying attribute result.'), t('Ubercart'));
++
++    // Check the attributes' integrity.
++    foreach ($loaded_product_attributes as $aid => $loaded_product_attribute) {
++      foreach (self::_attributeFieldsToTest('product') as $field) {
++        if ($loaded_product_attributes[$aid]->$field != $product_attributes[$aid]->$field) {
++          $this->fail(t('Attribute integrity check failed.'), t('Ubercart'));
++          break;
++        }
++      }
++    }
++
++    // Make sure all the options are on attributes correctly.
++    foreach ($all_options as $aid => $options) {
++      foreach ($options as $oid => $option) {
++        if (empty($loaded_product_attributes[$aid]) || empty($loaded_product_attributes[$aid]->options[$oid])) continue;
++
++        foreach (self::_attributeOptionFieldsToTest() as $field) {
++          if ($option->$field != $loaded_product_attributes[$aid]->options[$oid]->$field) {
++            $this->fail(t('Option integrity check failed.'), t('Ubercart'));
++            break;
++          }
++        }
++      }
++    }
++
++    // Add the selected attributes to the product.
++    foreach ($loaded_attributes as $loaded_attribute) {
++      uc_attribute_subject_save($loaded_attribute, 'class', $product_class->pcid, TRUE);
++    }
++
++    // Test loading all product attributes. (This covers uc_attribute_load_product_attributes(),
++    // as the semantics are the same -cha0s)
++    $loaded_class_attributes = uc_attribute_load_multiple(array(), 'class', $product_class->pcid);
++
++    // We'll get all in $loaded_attributes above, plus the original.
++    $class_attributes = $loaded_attributes;
++
++    // Make sure we only got the attributes we asked for. No more, no less.
++    $this->assertEqual(count($loaded_class_attributes), count($class_attributes), t('Verifying attribute result.'), t('Ubercart'));
++    $this->assertEqual(count($loaded_class_attributes), count(array_intersect_key($loaded_class_attributes, $class_attributes)), t('Verifying attribute result.'), t('Ubercart'));
++
++    // Check the attributes' integrity.
++    foreach ($loaded_class_attributes as $aid => $loaded_class_attribute) {
++      foreach (self::_attributeFieldsToTest('class') as $field) {
++        if ($loaded_class_attributes[$aid]->$field != $class_attributes[$aid]->$field) {
++          $this->fail(t('Attribute integrity check failed.'), t('Ubercart'));
++          break;
++        }
++      }
++    }
++
++    // Make sure all the options are on attributes correctly.
++    foreach ($all_options as $aid => $options) {
++      foreach ($options as $oid => $option) {
++        if (empty($loaded_class_attributes[$aid]) || empty($loaded_class_attributes[$aid]->options[$oid])) continue;
++
++        foreach (self::_attributeOptionFieldsToTest() as $field) {
++          if ($option->$field != $loaded_class_attributes[$aid]->options[$oid]->$field) {
++            $this->fail(t('Option integrity check failed.'), t('Ubercart'));
++            break;
++          }
++        }
++      }
++    }
++
++    // Test deletion of base attribute.
++    $aid = $attribute->aid;
++    $options = $attribute->options;
++    uc_attribute_delete($attribute->aid);
++
++    $this->assertFalse(uc_attribute_load($attribute->aid), t('Attribute was deleted properly.'), t('Ubercart'));
++
++    // Sanity check!
++    $this->assertFalse(db_result(db_query("SELECT aid FROM {uc_attributes} WHERE aid = %d", $attribute->aid)), t('Attribute was seriously deleted properly!'), t('Ubercart'));
++
++    // Test that options were deleted properly.
++    foreach ($options as $option) {
++      $this->assertFalse(db_result(db_query("SELECT oid FROM {uc_attribute_options} WHERE oid = %d", $option->oid)), t('Make sure options are deleted properly.'), t('Ubercart'));
++    }
++
++    // Test the deletion applied to products too.
++    $loaded_product_attributes = uc_attribute_load_multiple(array(), 'product', $product->nid);
++
++    // We'll get all in $loaded_attributes above, without the original. (Which
++    // has been deleted.)
++    $product_attributes = $loaded_attributes;
++
++    // Make sure we only got the attributes we asked for. No more, no less.
++    $this->assertEqual(count($loaded_product_attributes), count($product_attributes), t('Verifying attribute result.'), t('Ubercart'));
++    $this->assertEqual(count($loaded_product_attributes), count(array_intersect_key($loaded_product_attributes, $product_attributes)), t('Verifying attribute result.'), t('Ubercart'));
++
++    // Test the deletion applied to classes too.
++    $loaded_class_attributes = uc_attribute_load_multiple(array(), 'class', $product_class->pcid);
++
++    // We'll get all in $loaded_attributes above, without the original. (Which
++    // has been deleted.)
++    $class_attributes = $loaded_attributes;
++
++    // Make sure we only got the attributes we asked for. No more, no less.
++    $this->assertEqual(count($loaded_class_attributes), count($class_attributes), t('Verifying attribute result.'), t('Ubercart'));
++    $this->assertEqual(count($loaded_class_attributes), count(array_intersect_key($loaded_class_attributes, $class_attributes)), t('Verifying attribute result.'), t('Ubercart'));
++
++    // Add some adjustments.
++    self::createProductAdjustment(array('combination' => 'a:1:{i:1;s:1:"1";}', 'nid' => 1));
++    self::createProductAdjustment(array('combination' => 'a:1:{i:1;s:1:"2";}', 'nid' => 1));
++    self::createProductAdjustment(array('combination' => 'a:1:{i:1;s:1:"3";}', 'nid' => 1));
++    self::createProductAdjustment(array('combination' => 'a:1:{i:2;s:1:"1";}', 'nid' => 2));
++    self::createProductAdjustment(array('combination' => 'a:1:{i:3;s:1:"1";}', 'nid' => 2));
++    self::createProductAdjustment(array('combination' => 'a:1:{i:1;s:1:"2";}', 'nid' => 3));
++    self::createProductAdjustment(array('combination' => 'a:1:{i:1;s:1:"3";}', 'nid' => 3));
++    self::createProductAdjustment(array('combination' => 'a:1:{i:3;s:1:"2";}', 'nid' => 3));
++    self::createProductAdjustment(array('combination' => 'a:1:{i:3;s:1:"3";}', 'nid' => 4));
++
++    // Test deletion by nid.
++    uc_attribute_adjustments_delete(array('nid' => 1));
++    $this->assertEqual(6, db_result(db_query("SELECT COUNT(*) FROM {uc_product_adjustments}")), t('Ubercart'));
++
++    // Test deletion by aid.
++    uc_attribute_adjustments_delete(array('aid' => 2));
++    $this->assertEqual(5, db_result(db_query("SELECT COUNT(*) FROM {uc_product_adjustments}")), t('Ubercart'));
++
++    // Test deletion by oid.
++    uc_attribute_adjustments_delete(array('oid' => 2));
++    $this->assertEqual(3, db_result(db_query("SELECT COUNT(*) FROM {uc_product_adjustments}")), t('Ubercart'));
++
++    // Test deletion by aid and oid.
++    uc_attribute_adjustments_delete(array('aid' => 1, 'oid' => 3));
++    $this->assertEqual(2, db_result(db_query("SELECT COUNT(*) FROM {uc_product_adjustments}")), t('Ubercart'));
++  }
++
++  public function testAttributeUIAddAttribute() {
++    $this->drupalGet('admin/store/attributes/add');
++
++    $this->AssertText(t('The name of the attribute used in administrative forms'), t('Attribute add form working.'), t('Ubercart'));
++
++    $edit = (array) self::createAttribute(array(), FALSE);
++
++    $this->drupalPost('admin/store/attributes/add', $edit, t('Submit'));
++
++    $this->assertRaw('<td class="active">'. $edit['name'] .'</td>', t('Verify name field.'), t('Ubercart'));
++    $this->assertRaw('<td>'. $edit['label'] .'</td>', t('Verify label field.'), t('Ubercart'));
++    $this->assertRaw('<td>'. $edit['required'] ? t('Yes') : t('No') .'</td>', t('Verify required field.'), t('Ubercart'));
++    $this->assertRaw('<td align="center">'. $edit['ordering'] .'</td>', t('Verify ordering field.'), t('Ubercart'));
++    $types = _uc_attribute_display_types();
++    $this->assertRaw('<td>'. $types[$edit['display']] .'</td>', t('Verify ordering field.'), t('Ubercart'));
++
++    $attribute = uc_attribute_load($edit['aid']);
++
++    $fields_ok = TRUE;
++    foreach ($edit as $field => $value) {
++      if ($attribute->$field != $value) {
++        $this->showVar($attribute);
++        $this->showVar($edit);
++        $fields_ok = FALSE;
++        break;
++      }
++    }
++  }
++
++  public function testAttributeUISettings() {
++    $product = UbercartProductTestCase::createProduct();
++    $attribute = self::createAttribute(array(
++      'display' => 1,
++    ));
++
++    $option = self::createAttributeOption(array(
++      'price' => 30,
++    ));
++
++    $attribute->options[$option->oid] = $option;
++    uc_attribute_subject_save($attribute, 'product', $product->nid, TRUE);
++
++    $context = array(
++      'revision' => 'formatted',
++      'location' => 'product-attribute-form-element',
++      'subject' => array('attribute' => $attribute),
++      'extras' => array('option' => $option),
++    );
++
++    $qty = $product->default_qty;
++    if (!$qty) {
++      $qty = 1;
++    }
++
++    $price_info = array(
++      'price' => $option->price,
++      'qty' => $qty,
++    );
++    $adjust_price = uc_price($price_info, $context);
++
++    $price_info['price'] += $product->sell_price;
++    $total_price = uc_price($price_info, $context);
++
++    $raw = array(
++      'none' => $option->name .'</option>',
++      'adjustment' => $option->name .', +'. $adjust_price .'</option>',
++      'total' => $total_price .'</option>',
++    );
++
++    foreach (array('none', 'adjustment', 'total') as $type) {
++      $edit['uc_attribute_option_price_format'] = $type;
++      $this->drupalPost('admin/store/settings/attributes', $edit, t('Save configuration'));
++
++      $this->drupalGet('node/'. $product->nid);
++      $this->AssertRaw($raw[$type], t('Attribute option pricing is correct.'), t('Ubercart'));
++    }
++  }
++
++  public function testAttributeUIEditAttribute() {
++    $attribute = self::createAttribute();
++
++    $this->drupalGet('admin/store/attributes/'. $attribute->aid .'/edit');
++
++    $this->AssertText(t('Edit attribute: @name', array('@name' => $attribute->name)), t('Attribute edit form working.'), t('Ubercart'));
++
++    $edit = (array) self::createAttribute(array(), FALSE);
++    $this->drupalPost('admin/store/attributes/'. $attribute->aid .'/edit', $edit, t('Submit'));
++
++    $attribute = uc_attribute_load($attribute->aid);
++
++    $fields_ok = TRUE;
++    foreach ($edit as $field => $value) {
++      if ($attribute->$field != $value) {
++        $this->showVar($attribute);
++        $this->showVar($edit);
++        $fields_ok = FALSE;
++        break;
++      }
++    }
++
++    $this->AssertTrue($fields_ok, t('Attribute edited properly.'), t('Ubercart'));
++  }
++
++  public function testAttributeUIDeleteAttribute() {
++    $attribute = self::createAttribute();
++
++    $this->drupalGet('admin/store/attributes/'. $attribute->aid .'/delete');
++
++    $this->AssertText(t('Are you sure you want to delete the attribute @name?', array('@name' => $attribute->name)), t('Attribute delete form working.'), t('Ubercart'));
++
++    $edit = (array) self::createAttribute();
++    unset($edit['aid']);
++
++    $this->drupalPost('admin/store/attributes/'. $attribute->aid .'/delete', array(), t('Delete'));
++
++    $this->AssertText(t('Product attribute deleted.'), t('Attribute deleted properly.'), t('Ubercart'));
++  }
++
++  public function testAttributeUIAttributeOptions() {
++    $attribute = self::createAttribute();
++    $option = self::createAttributeOption();
++
++    uc_attribute_option_save($option);
++
++    $this->drupalGet('admin/store/attributes/'. $attribute->aid .'/options');
++
++    $this->AssertText(t('Options for @name', array('@name' => $attribute->name)), t('Attribute options form working.'), t('Ubercart'));
++  }
++
++  public function testAttributeUIAttributeOptionsAdd() {
++    $attribute = self::createAttribute();
++
++    $this->drupalGet('admin/store/attributes/'. $attribute->aid .'/options/add');
++
++    $this->AssertText(t('Options for @name', array('@name' => $attribute->name)), t('Attribute options add form working.'), t('Ubercart'));
++
++    $edit = (array) self::createAttributeOption(array(), FALSE);
++    unset($edit['aid']);
++
++    $this->drupalPost('admin/store/attributes/'. $attribute->aid .'/options/add', $edit, t('Submit'));
++
++    $option = db_fetch_object(db_query("SELECT * FROM {uc_attribute_options} WHERE aid = %d", $attribute->aid));
++
++    $fields_ok = TRUE;
++    foreach ($edit as $field => $value) {
++      if ($option->$field != $value) {
++        $this->showVar($option);
++        $this->showVar($edit);
++        $fields_ok = FALSE;
++        break;
++      }
++    }
++
++    $this->assertTrue($fields_ok, t('Attribute option added successfully by form.'), t('Ubercart'));
++  }
++
++  public function testAttributeUIAttributeOptionsEdit() {
++    $attribute = self::createAttribute();
++    $option = self::createAttributeOption();
++
++    uc_attribute_option_save($option);
++
++    $this->drupalGet('admin/store/attributes/'. $attribute->aid .'/options/'. $option->oid .'/edit');
++
++    $this->AssertText(t('Edit option: @name', array('@name' => $option->name)), t('Attribute options edit form working.'), t('Ubercart'));
++
++    $edit = (array) self::createAttributeOption(array(), FALSE);
++    unset($edit['aid']);
++    $this->drupalPost('admin/store/attributes/'. $attribute->aid .'/options/'. $option->oid .'/edit', $edit, t('Submit'));
++
++    $option = uc_attribute_option_load($option->oid);
++
++    $fields_ok = TRUE;
++    foreach ($edit as $field => $value) {
++      if ($option->$field != $value) {
++        $this->showVar($option);
++        $this->showVar($edit);
++        $fields_ok = FALSE;
++        break;
++      }
++    }
++
++    $this->assertTrue($fields_ok, t('Attribute option edited successfully by form.'), t('Ubercart'));
++  }
++
++  public function testAttributeUIAttributeOptionsDelete() {
++    $attribute = self::createAttribute();
++    $option = self::createAttributeOption();
++
++    uc_attribute_option_save($option);
++
++    $this->drupalGet('admin/store/attributes/'. $attribute->aid .'/options/'. $option->oid .'/delete');
++
++    $this->AssertText(t('Are you sure you want to delete the option @name?', array('@name' => $option->name)), t('Attribute options delete form working.'), t('Ubercart'));
++
++    $this->drupalPost('admin/store/attributes/'. $attribute->aid .'/options/'. $option->oid .'/delete', array(), t('Delete'));
++
++    $option = uc_attribute_option_load($option->oid);
++
++    $this->assertFalse($option, t('Attribute option deleted successfully by form'), t('Ubercart'));
++  }
++
++  // Simpletest needs work before we can do this form.
++/*  public function testAttributeUIClassAttributeOverview() {
++    $class = UbercartProductTestCase::createProductClass();
++    $attribute = self::createAttribute();
++
++    $this->drupalGet('admin/store/products/classes/'. $class->pcid .'/attributes');
++
++    $this->assertText(t('You must first add attributes to this class.'), t('Class attribute form working.'), t('Ubercart'));
++
++    uc_attribute_subject_save($attribute, 'class', $class->pcid);
++
++    $this->drupalGet('admin/store/products/classes/'. $class->pcid .'/attributes');
++
++    $this->assertNoText(t('You must first add attributes to this class.'), t('Class attribute form working.'), t('Ubercart'));
++
++    $a = (array) self::createAttribute(array(), FALSE);
++    unset($a['name'], $a['description']);
++    $edit['attributes'][$attribute->aid] = $a;
++    $this->showVar($edit);
++    $this->drupalPost('admin/store/products/classes/'. $class->pcid .'/attributes', $edit, t('Save changes'));
++
++    $attribute = uc_attribute_load($attribute->aid, 'class', $class->pcid);
++
++    $fields_ok = TRUE;
++    foreach ($a as $field => $value) {
++      if ($attribute->$field != $value) {
++        $this->showVar($attribute);
++        $this->showVar($a);
++        $fields_ok = FALSE;
++        break;
++      }
++    }
++
++    $this->assertTrue($fields_ok, t('Class attribute edited successfully by form.'), t('Ubercart'));
++
++    $edit = array();
++    $edit['attributes'][$attribute->aid]['remove'] = TRUE;
++    $this->drupalPost('admin/store/products/classes/'. $class->pcid .'/attributes', $edit, t('Save changes'));
++
++    $this->assertText(t('You must first add attributes to this class.'), t('Class attribute form working.'), t('Ubercart'));
++  }
++
++  public function testAttributeUIClassAttributeAdd() {
++    $class = UbercartProductTestCase::createProductClass();
++    $attribute = self::createAttribute();
++
++    $this->drupalGet('admin/store/products/classes/'. $class->pcid .'/attributes/add');
++
++    $this->assertRaw(t('@attribute</option>', array('@option' => $attribute->name)), t('Class attribute add form working.'), t('Ubercart'));
++
++    $edit['add_attributes'][$attribute->aid] = $attribute->aid;
++
++    $this->drupalPost('admin/store/products/classes/'. $class->pcid .'/attributes/add', $edit, t('Add atrributes'));
++
++    $this->assertNoText(t('You must first add attributes to this class.'), t('Class attribute form working.'), t('Ubercart'));
++  }
++
++  public function testAttributeUIClassAttributeOptionOverview() {
++    $class = UbercartProductTestCase::createProductClass();
++    $attribute = self::createAttribute();
++    $option = self::createAttributeOption();
++
++    uc_attribute_subject_save($attribute, 'class', $class->pcid);
++
++    $this->drupalGet('admin/store/products/classes/'. $class->pcid .'/options');
++
++    $this->assertRaw(t('<h2>@attribute</h2>', array('@attribute' => $attribute->name)), t('Class attribute option form working.'), t('Ubercart'));
++
++    $o = (array) self::createAttribute(array(), FALSE);
++    unset($o['name'], $o['aid']);
++    $edit['attributes'][$attribute->aid]['options'][$option->oid] = $o;
++    $this->showVar($edit);
++    $this->drupalPost('admin/store/products/classes/'. $class->pcid .'/options', $edit, t('Submit'));
++
++    $option = uc_attribute_subject_option_load($option->oid, 'class', $class->pcid);
++
++    $fields_ok = TRUE;
++    foreach ($o as $field => $value) {
++      if ($option->$field != $value) {
++        $this->showVar($option);
++        $this->showVar($o);
++        $fields_ok = FALSE;
++        break;
++      }
++    }
++    $this->assertTrue($fields_ok, t('Class attribute edited successfully by form.'), t('Ubercart'));
++  }
++  */
++
++  public static function createProductAdjustment($data) {
++    $adjustment = $data + array(
++      'nid' => rand(1, db_last_insert_id('node', 'nid')),
++      'model' => self::randomName(8),
++    );
++    db_query("INSERT INTO {uc_product_adjustments} (nid, combination, model) VALUES (%d, '%s', '%s')", $data['nid'], $data['combination'], $data['model']);
++  }
++
++  protected static function _attributeFieldsToTest($type = '') {
++    $fields = array(
++      'aid', 'name', 'ordering', 'required', 'display', 'description', 'label',
++    );
++
++    switch ($type) {
++      case 'product':
++      case 'class':
++
++        $info = uc_attribute_type_info($type);
++        $fields = array_merge($fields, array($info['id']));
++      break;
++    }
++    return $fields;
++  }
++
++  protected static function _attributeOptionFieldsToTest($type = '') {
++    $fields = array(
++      'aid', 'oid', 'name', 'cost', 'price', 'weight', 'ordering',
++    );
++
++    switch ($type) {
++      case 'product':
++      case 'class':
++
++        $info = uc_attribute_type_info($type);
++        $fields = array_merge($fields, array($info['id']));
++      break;
++    }
++    return $fields;
++  }
++
++  public static function createAttribute($data = array(), $save = TRUE) {
++    $attribute = $data + array(
++      'name' => DrupalWebTestCase::randomName(8),
++      'label' => DrupalWebTestCase::randomName(8),
++      'description' => DrupalWebTestCase::randomName(8),
++      'required' => rand(0, 1) ? TRUE : FALSE,
++      'display' => rand(0, 3),
++      'ordering' => rand(-10, 10),
++    );
++    $attribute = (object) $attribute;
++
++    if ($save) {
++      uc_attribute_save($attribute);
++    }
++    return $attribute;
++  }
++
++  public static function createAttributeOption($data = array(), $save = TRUE) {
++    $option = $data + array(
++      'aid' => rand(1, db_last_insert_id('uc_attributes', 'aid')),
++      'name' => DrupalWebTestCase::randomName(8),
++      'cost' => rand(-500, 500),
++      'price' => rand(-500, 500),
++      'weight' => rand(-500, 500),
++      'ordering' => rand(-10, 10),
++    );
++    $option = (object) $option;
++
++    if ($save) {
++      uc_attribute_option_save($option);
++    }
++    return $option;
++  }
++
++  function showVar($var) {
++    $this->pass('<pre>'. print_r($var, TRUE) .'</pre>');
++  }
++}
+diff --git uc_product/uc_product.admin.inc uc_product/uc_product.admin.inc
+index 2af698e..8b7aa9f 100644
+--- uc_product/uc_product.admin.inc
++++ uc_product/uc_product.admin.inc
+@@ -389,18 +389,11 @@ function uc_product_features($node) {
+   if (arg(4)) {
+     // First check to see if we're trying to remove a feature.
+     if (intval(arg(5)) > 0 && arg(6) == 'delete') {
+-      $result = db_query("SELECT * FROM {uc_product_features} WHERE pfid = %d AND fid = '%s'", intval(arg(5)), arg(4));
+-      if ($feature = db_fetch_array($result)) {
++      $feature = uc_product_feature_load(intval(arg(5)), arg(4));
++      if (isset($feature)) {
+         // If the user confirmed the delete, process it!
+         if ($_POST['pf_delete']) {
+-          // Call the delete function for this product feature if it exists.
+-          $func = uc_product_feature_data($feature['fid'], 'delete');
+-          if (function_exists($func)) {
+-            $func($feature);
+-          }
+-
+-          // Remove the product feature data from the database.
+-          db_query("DELETE FROM {uc_product_features} WHERE pfid = %d", intval(arg(5)));
++          uc_product_feature_delete(intval(arg(5)));
+ 
+           drupal_set_message(t('The product feature has been deleted.'));
+           drupal_goto('node/'. arg(1) .'/edit/features');
+@@ -425,8 +418,8 @@ function uc_product_features($node) {
+         $output = drupal_get_form($func, $node, array());
+       }
+       elseif (intval(arg(5)) > 0) {
+-        $result = db_query("SELECT * FROM {uc_product_features} WHERE pfid = %d AND fid = '%s'", intval(arg(5)), arg(4));
+-        if ($feature = db_fetch_array($result)) {
++        $feature = uc_product_feature_load(intval(arg(5)), arg(4));
++        if (isset($feature)) {
+           $output = drupal_get_form($func, $node, $feature);
+         }
+       }
+@@ -446,27 +439,28 @@ function uc_product_features($node) {
+ 
+   $header = array(t('Type'), t('Description'), t('Operations'));
+ 
+-  $result = db_query("SELECT * FROM {uc_product_features} WHERE nid = %d ORDER BY pfid ASC", $node->nid);
+-  while ($feature = db_fetch_object($result)) {
+-    $operations = array(
+-      l(t('edit'), 'node/'. $node->nid .'/edit/features/'. $feature->fid .'/'. $feature->pfid),
+-      l(t('delete'), 'node/'. $node->nid .'/edit/features/'. $feature->fid .'/'. $feature->pfid .'/delete'),
+-    );
+-    $rows[] = array(
+-      'data' => array(
+-        array('data' => uc_product_feature_data($feature->fid, 'title'), 'nowrap' => 'nowrap'),
+-        array('data' => $feature->description, 'width' => '100%'),
+-        array('data' => implode(' ', $operations), 'nowrap' => 'nowrap'),
+-      ),
+-      'valign' => 'top',
+-    );
+-  }
+-
+-  if (empty($rows)) {
++  $features = uc_product_feature_load_multiple($node->nid);
++  if (empty($features)) {
+     $rows[] = array(
+       array('data' => t('No features found for this product.'), 'colspan' => 3),
+     );
+   }
++  else {
++    foreach ($features as $feature) {
++      $operations = array(
++        l(t('edit'), 'node/'. $node->nid .'/edit/features/'. $feature->fid .'/'. $feature->pfid),
++        l(t('delete'), 'node/'. $node->nid .'/edit/features/'. $feature->fid .'/'. $feature->pfid .'/delete'),
++      );
++      $rows[] = array(
++        'data' => array(
++          array('data' => uc_product_feature_data($feature->fid, 'title'), 'nowrap' => 'nowrap'),
++          array('data' => $feature->description, 'width' => '100%'),
++          array('data' => implode(' ', $operations), 'nowrap' => 'nowrap'),
++        ),
++        'valign' => 'top',
++      );
++    }
++  }
+ 
+   $output = theme('table', $header, $rows)
+           . drupal_get_form('uc_product_feature_add_form');
+diff --git uc_product/uc_product.module uc_product/uc_product.module
+index 069537a..36659d8 100644
+--- uc_product/uc_product.module
++++ uc_product/uc_product.module
+@@ -1948,6 +1948,65 @@ function uc_product_feature_save($data) {
+ }
+ 
+ /**
++ * Load all product feature for a node.
++ *
++ * @param $nid
++ *   The product node ID.
++ * @returns
++ *   The array of all product features object.
++ */
++function uc_product_feature_load_multiple($nid) {
++  $result = db_query("SELECT * FROM {uc_product_features} WHERE nid = %d ORDER BY pfid ASC", $nid);
++  while ($feature = db_fetch_object($result)) {
++    $features[$feature->pfid] = $feature;
++  }
++  return $features;
++}
++
++/**
++ * Load a product feature object.
++ *
++ * @todo: should return an object instead of array.
++ *
++ * @param $pfid
++ *   The product feature ID.
++ * @param $fid
++ *   Optional. Specify a specific feature id.
++ * @returns
++ *   The product feature object.
++ */
++function uc_product_feature_load($pfid, $fid = NULL) {
++  if (isset($fid)) {
++    $feature = db_fetch_array(db_query("SELECT * FROM {uc_product_features} WHERE pfid = %d AND fid = '%s'", $pfid, $fid)); 
++  }
++  else {
++    $feature = db_fetch_array(db_query("SELECT * FROM {uc_product_features} WHERE pfid = %d", $pfid));
++  }
++  return $feature;
++}
++
++/**
++ * Delete a product feature object.
++ *
++ * @param $pfid
++ *   The product feature ID.
++ * @returns
++ *   The product feature object.
++ */
++function uc_product_feature_delete($pfid) {
++  $feature = uc_product_feature_load($pfid);
++
++  // Call the delete function for this product feature if it exists.
++  $func = uc_product_feature_data($feature['fid'], 'delete');
++  if (function_exists($func)) {
++    $func($feature);
++  }
++  db_query("DELETE FROM {uc_product_features} WHERE pfid = %d", $pfid);
++
++  return SAVED_DELETED;
++}
++
++/**
+  * Create a file field with an image field widget, and attach it to products.
+  *
+  * This field is used by default on the product page, as well as on the cart
+diff --git uc_product/uc_product.test uc_product/uc_product.test
+new file mode 100644
+index 0000000..4068405
+--- /dev/null
++++ uc_product/uc_product.test
+@@ -0,0 +1,87 @@
++<?php
++// $Id$
++
++/**
++ * @file
++ * Ubercart Product Tests
++ */
++
++class UbercartProductTestCase extends DrupalWebTestCase {
++
++  var $admin_user;
++
++  function getInfo() {
++    return array(
++      'name' => t('Product API'),
++      'description' => t('Test the product API.'),
++      'group' => t('Ubercart'),
++    );
++  }
++
++  function setUp() {
++    parent::setUp('token', 'uc_store', 'uc_product');
++
++    $this->admin_user = $this->drupalCreateUser(array('administer store'));
++    $this->drupalLogin($this->admin_user);
++  }
++
++  /**
++   * @param $data
++   *   Data to potentially override the data used to create a product.
++   * @return (stdClass)
++   *  The product object.
++   */
++  public static function createProduct($data = array()) {
++
++    $weight_units = array(
++      'lb', 'kg', 'oz', 'g',
++    );
++    $weight_unit = $weight_units[array_rand($weight_units)];
++
++    $length_units = array(
++      'in', 'ft', 'cm', 'mm',
++    );
++    $length_unit = $length_units[array_rand($length_units)];
++
++    $product = $data + array(
++      'model' => DrupalWebTestCase::randomName(8),
++      'list_price' => rand(0, 1000),
++      'cost' => rand(0, 1000),
++      'sell_price' => rand(0, 1000),
++      'weight' => rand(0, 1000),
++      'weight_units' => $weight_unit,
++      'length' => rand(0, 1000),
++      'width' => rand(0, 1000),
++      'height' => rand(0, 1000),
++      'length_units' => $length_unit,
++      'pkg_qty' => rand(0, 50),
++      'default_qty' => rand(0, 50),
++      'ordering' => rand(-10, 10),
++      'shippable' => rand(0, 1),
++      'type' => 'product',
++      'title' => DrupalWebTestCase::randomName(8),
++      'uid' => 1,
++    );
++    $product = (object)$product;
++    $product->unique_hash = md5($product->vid . $product->nid . $product->model . $product->list_price . $product->cost . $product->sell_price . $product->weight . $product->weight_units . $product->length . $product->width . $product->height . $product->length_units . $product->pkg_qty . $product->default_qty . $product->shippable . time());
++
++    node_save($product);
++
++    return $product;
++  }
++
++  // Fix this after adding a proper API call for saving a product class.
++  public static function createProductClass($data = array()) {
++    $product_class = $data + array(
++      'pcid' => DrupalWebTestCase::randomName(8),
++      'name' => DrupalWebTestCase::randomName(8),
++      'description' => DrupalWebTestCase::randomName(8),
++    );
++    $product_class = (object) $product_class;
++
++    drupal_write_record('uc_product_classes', $product_class);
++
++    return $product_class;
++//    db_query("INSERT INTO {uc_product_classes} (pcid, name, description) VALUES ('%s', '%s', '%s')", $pcid, $form_state['values']['name'], $form_state['values']['description']);
++  }
++}
diff --git modules/uc_recurring_subscription/uc_recurring_subscription.admin.inc modules/uc_recurring_subscription/uc_recurring_subscription.admin.inc
new file mode 100644
index 0000000..ca96c4c
--- /dev/null
+++ modules/uc_recurring_subscription/uc_recurring_subscription.admin.inc
@@ -0,0 +1,861 @@
+<?php
+// $Id:$
+
+/**
+ * @file
+ * Uc recurring subscription UI.
+ */
+
+/**
+ * Create the subscription overiew page.
+ */
+function uc_recurring_subscription_overview() {
+
+  // Warn user if the ubercart API patch not applied.
+  if (!function_exists('uc_attribute_load_multiple')) {
+    drupal_set_message(t('This module requires a patch to ubercart, read the <a href="@readme">README.txt</a>', array(
+      '@readme' => url(drupal_get_path('module', 'uc_recurring_subscription') .'/README.txt')
+    )), 'error');
+    return '';
+  }
+
+  drupal_add_css(drupal_get_path('module', 'uc_recurring_subscription') .'/uc_recurring_subscription.css');
+  $product_class = variable_get('uc_recurring_subscription_product_class', 'uc_recurring_subscription');
+  $result = db_query("SELECT n.nid, n.title
+                      FROM {node} n
+                        LEFT JOIN {uc_recurring_subscription} rs ON rs.nid=n.nid
+                      WHERE n.type = '%s'
+                      ORDER BY rs.weight", $product_class);
+
+  $roles = user_roles(TRUE);
+
+  $form['products']['#tree'] = TRUE;
+  $form['products']['#theme'] = 'uc_recurring_subscription_products';
+  while($node = db_fetch_object($result)) {
+    $product = node_load($node->nid);
+    $attribute = uc_attribute_load(variable_get('uc_recurring_subscription_attribute', ''));
+
+    // roles
+    $product_roles = array();
+    if (!empty($product->subscription->access['subscribe_grant'])) {
+      $product_roles = array_intersect_key($roles, $product->subscription->access['subscribe_grant']);
+    }
+    // payment options
+    $products = _uc_recurring_subscription_get_product_features($node->nid);
+    $intervals = array();
+    foreach ($products as $id => $feature) {
+      $interval = uc_price($feature->fee_amount, array()) .' - '. _uc_recurring_subscription_create_attribute_option($feature->regular_interval_value, $feature->regular_interval_unit);
+
+      if ($product->attributes[$attribute->aid]->default_option == $feature->option->oid ) {
+        $interval = '<span class="default-option">'. $interval .'</span>';
+      }
+      if (module_exists('uc_cart_links')) {
+        $interval .= ' '. t('(<a href="@link">cart link</a>)', array('@link' => url('cart/add/e-p'. $node->nid .'_q1_a'. $attribute->aid .'o'. $feature->option->oid .'-isub', array('query' => 'destination=cart/checkout', 'absolute' => TRUE))));
+      }
+
+      if ($feature->regular_interval_value != $feature->initial_charge_value &&
+          $feature->regular_interval_unit != $feature->initial_charge_unit) {
+        $trial = uc_price($product->sell_price, array()) .' - '. $feature->initial_charge_value .' '. $feature->initial_charge_unit;
+      }
+
+      $intervals[] = $interval;
+    }
+
+    $form['products'][$node->nid]['title'] = array('#value' => l($product->title, 'node/'. $product->nid));
+    $form['products'][$node->nid]['role'] = array('#value' => implode(', ', $product_roles));
+    $form['products'][$node->nid]['trial'] = array('#value' => empty($trial) ? t('None') : $trial);
+    $form['products'][$node->nid]['interval'] = array('#value' => implode('<br/>', $intervals));
+    $form['products'][$node->nid]['operations'] = array(
+      '#value' => l('edit', 'admin/store/subscriptions/'. $node->nid .'/edit') .' | '. l('delete', 'node/'. $node->nid .'/delete', array('query' => 'destination=admin/store/subscriptions'))
+    );
+    $form['products'][$node->nid]['weight'] = array(
+      '#type' => 'weight',
+      '#delta' => 10,
+      '#default_value' => $product->subscription->weight,
+    );
+  }
+
+  $form['submit'] = array('#type' => 'submit', '#value' => t('Save order'));
+
+  return $form;
+}
+
+/**
+ * Update the weights of the products.
+ */
+function uc_recurring_subscription_overview_submit($form, &$form_state) {
+  foreach($form_state['values']['products'] as $nid => $product) {
+    db_query("UPDATE {uc_recurring_subscription} SET weight = %d WHERE nid = %d", $product['weight'], $nid);
+  }
+}
+
+/**
+ * Form to add a new payment interval for the product.
+ */
+function uc_recurring_subscription_product_form($form_state, $product_id = NULL) {
+  drupal_add_js(drupal_get_path('module', 'uc_recurring_subscription') .'/uc_recurring_subscription.js', 'module');
+  drupal_add_css(drupal_get_path('module', 'uc_recurring_subscription') .'/uc_recurring_subscription.css');
+
+  $form = array();
+  if (isset($product_id)) {
+    $form['product_id'] = array(
+      '#type' => 'hidden',
+      '#value' => $product_id,
+    );
+    $node = node_load($product_id);
+    $products = _uc_recurring_subscription_get_product_features($node->nid);
+  }
+  $form['product'] = array(
+    '#type' => 'fieldset',
+    '#title' => 'Step One: Product Details',
+  );
+  $form['product']['title'] = array(
+    '#type' => 'textfield',
+    '#title' => 'Title',
+    '#required' => TRUE,
+    '#default_value' => $node->title,
+  );
+  $form['product']['body'] = array(
+    '#type' => 'textarea',
+    '#title' => 'Description',
+    '#default_value' => $node->body,
+  );
+
+  $form['recurring'] = array(
+    '#type' => 'fieldset',
+    '#title' => 'Step Two: Define Subscription Details',
+    '#description' => t('By adding multiple payment options, users will be given the option to choose from one of these payment options, this allows you to specify different prices and payment plans for this subscription product'),
+  );
+  $form['recurring']['trial'] = array(
+    '#type' => 'fieldset',
+    '#title' => 'Add trial',
+    '#collapsible' => TRUE,
+    '#collapsed' => TRUE,
+  );
+  // We look at the default option for the trial information.
+  $aid = variable_get('uc_recurring_subscription_attribute', '');
+  if (!empty($aid)) {
+    $default_option = $node->attributes[$aid]->default_option;
+    $name = $node->attributes[$aid]->options[$default_option]->name;
+    foreach($products as $product) {
+      $num = $product->regular_interval_value;
+      $unit = $product->regular_interval_unit;
+      if ($name == _uc_recurring_subscription_create_attribute_option($num, $unit)) {
+        // This is the default product, the price and initial interval should
+        // be different if there is a trial.
+        if ($node->sell_price != $product->fee_amount ||
+            $product->regular_interval_value != $product->initial_charge_value ||
+            $product->regular_interval_unit != $product->initial_charge_unit) {
+          $trial_product = $product;
+          $form['recurring']['trial']['#collapsed'] = FALSE;
+        }
+        break;
+      }
+    }
+  }
+  $form['recurring']['trial']['trial_amount'] = array(
+    '#type' => 'textfield',
+    '#title' => 'Trial Amount',
+    '#default_value' => !empty($trial_product) ? $node->sell_price : '',
+    '#size' => 20,
+  );
+  $form['recurring']['trial']['trial_interval_value'] = array(
+    '#type' => 'textfield',
+    '#default_value' => $trial_product->initial_charge_value,
+    '#size' => 2,
+    '#prefix' => '<div class="subscription-interval-value">',
+    '#suffix' => '</div>',
+  );
+  $form['recurring']['trial']['trial_interval_unit'] = array(
+    '#type' => 'select',
+    '#options' => array(
+      'days' => t('day(s)'),
+      'weeks' => t('week(s)'),
+      'months' => t('month(s)'),
+      'years' => t('year(s)'),
+    ),
+    '#default_value' => $trial_product->initial_charge_unit,
+    '#prefix' => '<div class="subscription-interval-period">',
+    '#suffix' => '</div>',
+  );
+
+  $form['recurring']['add'] = array(
+    '#type' => 'button',
+    '#value' => 'Add a payment option',
+    '#ahah' => array(
+      'event' => 'click',
+      'path' => 'subscriptions/ahah/add_interval',
+      'wrapper' => 'recurring_intervals',
+      'method' => 'replace',
+      'effect' => 'fade',
+      'progress' => array(
+        'type' => 'bar',
+      ),
+    ),
+    '#prefix' => '<div style="clear:both"></div>',
+  );
+
+  $form['recurring']['recurring_intervals'] = array(
+    '#value' => t(' '),
+    '#prefix' => '<div id="recurring_intervals">',
+    '#suffix' => '</div>',
+  );
+
+  if (isset($form_state['recurring_count'])) {
+    $recurring_count = $form_state['recurring_count'];
+  }
+  else {
+    $recurring_count = count($products);
+  }
+  $form['recurring']['recurring_intervals']['#theme'] = 'uc_recurring_subscription_item';
+  if ($recurring_count > 0) {
+    $delta = 0;
+    foreach ($products as $product) {
+      $form['recurring']['recurring_intervals'][$delta] = _uc_recurring_subscription_add_interval_form($delta, $product);
+      $default_options[$delta] = '';
+      if (isset($product) && $product->option->default_option) {
+        $default_option = $delta;
+      }
+      $delta++;
+    }
+  }
+  $form['recurring']['recurring_intervals']['default_payment'] = array(
+    '#type' => 'radios',
+    '#options' => $default_options,
+    '#default_value' => $default_option,
+  );
+
+  $form['access'] = array(
+    '#type' => 'fieldset',
+    '#title' => 'Step Three: Access & Permissions',
+    '#description' => t('In this section you can define the access that is granted or removed on subscription creation, renewal and expiration events.'),
+  );
+
+  // If uc_roles is enabled we will use the settings from this.
+  $roles = array();
+  if (module_exists('uc_roles')) {
+    $roles = _uc_roles_get_choices();
+  }
+  else if (user_access('administer permissions')) {
+    $roles = user_roles(TRUE);
+    unset($roles[DRUPAL_AUTHENTICATED_RID]);
+  }
+
+  $form['access']['role'] = array(
+    '#type' => 'fieldset',
+    '#title' => 'Roles',
+    '#description' => t('Select the role(s) which are assigned to members who subscribe to this subscription product and then removed when the subscription expires.'),
+    '#collapsible' => TRUE,
+    '#collapsed' => TRUE,
+  );
+  if (empty($roles)) {
+    $form['access']['role']['message'] = array(
+      '#value' => t('You need to first add a user role <a href="@role_link">here</a>.', array('@role_link' => url('admin/user/roles'))),
+    );
+  }
+  else {
+    $form['access']['role']['#theme'] = 'uc_recurring_subscription_role_items';
+    $form['access']['role']['subscribe_grant'] = array(
+      '#type' => 'select',
+      '#options' => $roles,
+      '#multiple' => TRUE,
+      '#default_value' => $node->subscription->access['subscribe_grant'],
+      '#attributes' => array('class' => 'role-option'),
+      '#parents' => array('access', 0, 'subscribe_grant'),
+    );
+    $form['access']['role']['expire_grant'] = array(
+      '#type' => 'select',
+      '#options' => $roles,
+      '#multiple' => TRUE,
+      '#default_value' => $node->subscription->access['expire_grant'],
+      '#attributes' => array('class' => 'role-option'),
+      '#parents' => array('access', 0, 'expire_grant'),
+    );
+    $form['access']['role']['expire_revoke'] = array(
+      '#type' => 'select',
+      '#options' => $roles,
+      '#multiple' => TRUE,
+      '#default_value' => $node->subscription->access['expire_revoke'],
+      '#attributes' => array('class' => 'role-option'),
+      '#parents' => array('access', 0, 'expire_revoke'),
+    );
+  }
+
+  if (module_exists('uc_og_subscribe')) {
+    $form['access']['og'] = array(
+      '#type' => 'fieldset',
+      '#title' => 'Organic Groups',
+      '#description' => t('Select which organic groups a user should be subscribed to on a new subscripion purchased or unsubscribed when the subscription expires.'),
+      '#collapsible' => TRUE,
+      '#collapsed' => TRUE,
+      '#theme' => 'uc_recurring_subscription_og_items',
+    );
+    $groups = uc_og_subscribe_get_groups();
+    $form['access']['og']['subscribe_gid'] = array(
+      '#type' => 'select',
+      '#default_value' => $node->subscription->access['subscribe_gid'],
+      '#options' => $groups,
+      '#multiple' => TRUE,
+      '#parents' => array('access', 0, 'subscribe_gid'),
+    );
+    $form['access']['og']['unsubscribe_gid'] = array(
+      '#type' => 'select',
+      '#default_value' => $node->subscription->access['unsubscribe_gid'],
+      '#options' => $groups,
+      '#multiple' => TRUE,
+      '#parents' => array('access', 0, 'unsubscribe_gid'),
+    );
+  }
+
+/*
+  $form['email'] = array(
+    '#type' => 'fieldset',
+    '#title' => 'Step Four: Email/Notifications',
+    '#description' => t('Setup emails to be sent on various events.'),
+  );
+  $form['email']['#theme'] = 'uc_recurring_subscription_email_options';
+  $form['email']['notication_event'] = array(
+    '#type' => 'select',
+    '#title' => t('Add new email on event'),
+    '#options' => array(),
+  );
+  $form['email']['add_notification'] = array('#type' => 'submit', '#value' => t('Add email'));
+*/
+
+  if (!empty($node->nid) && $node->type != variable_get('uc_recurring_subscription_product_class', 'uc_recurring_subscription')) {
+    return confirm_form($form, '', uc_referer_uri(), t('This product is not a subscription product, saving this form will convert this product to a subscription product, which could result in data being lost if you have specified custom fields.'), t('Save product and convert to a subscription'), t('Cancel'));
+  }
+  else {
+    $form['submit'] = array('#type' => 'submit', '#value' => t('Save'));
+  }
+
+  if (!isset($product_id)) {
+    $form['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
+  }
+
+  return $form;
+}
+
+/**
+ * This is just a helper function for handling ahah callbacks.
+ */
+function uc_recurring_subscription_ahah($context) {
+  $func = '_uc_recurring_subscription_ahah_'. $context;
+  $form_state['values'] = $_POST;
+  if (function_exists($func)) {
+    $func($form_state);
+  }
+  exit();
+}
+
+/**
+ * Creates the payment interval form elements.
+ */
+function _uc_recurring_subscription_add_interval_form($delta, $product = NULL) {
+  $form = array(
+    '#tree' => TRUE,
+  );
+  $form['pfid'] = array(
+    '#type' => 'hidden',
+    '#value' => $product->pfid,
+    '#parents' => array('recurring_intervals', $delta, 'pfid'),
+  );
+  $form['interval_value'] = array(
+    '#type' => 'textfield',
+    '#default_value' => $product->regular_interval_value,
+    '#size' => 2,
+    '#prefix' => '<div class="subscription-interval-value">',
+    '#suffix' => '</div>',
+    '#parents' => array('recurring_intervals', $delta, 'interval_value'),
+  );
+  $form['interval_unit'] = array(
+    '#type' => 'select',
+    '#options' => array(
+      'days' => t('day(s)'),
+      'weeks' => t('week(s)'),
+      'months' => t('month(s)'),
+      'years' => t('year(s)'),
+    ),
+    '#default_value' => $product->regular_interval_unit,
+    '#prefix' => '<div class="subscription-interval-period">',
+    '#suffix' => '</div>',
+    '#parents' => array('recurring_intervals', $delta, 'interval_unit'),
+  );
+  $form['amount'] = array(
+    '#type' => 'textfield',
+    '#default_value' => $product->fee_amount,
+    '#size' => 10,
+    '#prefix' => '<div class="subscription-amount">',
+    '#suffix' => '</div>',
+    '#parents' => array('recurring_intervals', $delta, 'amount'),
+  );
+  $form['number_intervals'] = array(
+    '#type' => 'textfield',
+    '#default_value' => $product->number_intervals < 0 ? '' : $product->number_intervals,
+    '#attributes' => $product->number_intervals < 0 ? array('disabled' => 'disabled') : array(),
+    '#size' => 10,
+    '#prefix' => '<div class="subscription-num-intervals">',
+    '#suffix' => '</div>',
+    '#parents' => array('recurring_intervals', $delta, 'number_intervals'),
+  );
+  $attributes['class'] = 'unlimited-checkbox';
+  if ($product->number_intervals < 0) {
+    $attributes['checked'] = 'checked';
+  }
+  $form['unlimited'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Unlimited'),
+    '#attributes' => $attributes,
+    '#prefix' => '<div class="subscription-unlimited-intervals">',
+    '#suffix' => '</div>',
+    '#parents' => array('recurring_intervals', $delta, 'unlimited'),
+  );
+
+  $form['weight'] = array(
+    '#type' => 'weight',
+    '#delta' => 50,
+    '#default_value' => $product->option->ordering,
+    '#parents' => array('recurring_intervals', $delta, 'weight'),
+  );
+  $form['operations'] = array(
+    '#type' => 'button',
+    '#value' => t('remove'),
+    '#parents' => array('recurring_intervals', $delta, 'operations'),
+    '#ahah' => array(
+      'event' => 'click',
+      'path' => 'subscriptions/ahah/remove_interval/'. $delta,
+      'wrapper' => 'recurring_intervals',
+      'method' => 'replace',
+      'progress' => array(
+        'type' => 'throbber',
+      ),
+    ),
+  );
+  return $form;
+}
+
+/**
+ * Adds a payment intervals option.
+ */
+function _uc_recurring_subscription_ahah_add_interval() {
+  $delta = sizeof($_POST['recurring_intervals']) ? max(array_keys($_POST['recurring_intervals']))+1 : 0;
+  $fields = _uc_recurring_subscription_add_interval_form($delta);
+
+  $form_state = array('submitted' => FALSE);
+  $form_build_id = $_POST['form_build_id'];
+  // Add the new element to the stored form. Without adding the element to the
+  // form, Drupal is not aware of this new elements existence and will not
+  // process it. We retreive the cached form, add the element, and resave.
+  $form = form_get_cache($form_build_id, $form_state);
+  $form['recurring']['recurring_intervals'][$delta] = $fields;
+  form_set_cache($form_build_id, $form, $form_state);
+  $form += array(
+    '#post' => $_POST,
+    '#programmed' => FALSE,
+  );
+  // Rebuild the form.
+  $form = form_builder($_POST['form_id'], $form, $form_state);
+
+  // Render the new output.
+  $new_form = $form['recurring']['recurring_intervals'];
+  $output = drupal_render($new_form);
+
+  drupal_json(array('data' => $output, 'status' => true));
+}
+
+/**
+ * Removes a payment interval option.
+ */
+function _uc_recurring_subscription_ahah_remove_interval() {
+  $form_state = array('submitted' => FALSE);
+  $form_build_id = $_POST['form_build_id'];
+
+  $delta = arg(3);
+  $form = form_get_cache($form_build_id, $form_state);
+  $pfid = $form['recurring']['recurring_intervals'][$delta]['pfid']['#value'];
+  if (!empty($pfid)) {
+    uc_product_feature_delete($pfid);
+  }
+  unset($form['recurring']['recurring_intervals'][$delta]);
+  // Rebuild the form.
+  $form = form_builder($_POST['form_id'], $form, $form_state);
+
+  // Render the new output.
+  $new_form = $form['recurring']['recurring_intervals'];
+  $output = drupal_render($new_form);
+
+  print drupal_to_js(array('data' => $output, 'status' => true));
+}
+
+/**
+ * Validates the subscription form submission.
+ */
+function uc_recurring_subscription_product_form_validate(&$form, &$form_state) {
+  // Make the changes we want to the form state.
+  if ($form_state['values']['recurring_intervals']) {
+    $form_state['recurring_count'] = count($form_state['values']['recurring_intervals']);
+  }
+
+  if ($form_state['values']['op'] == 'submit') {
+    if ($form_state['recurring_count'] <= 0) {
+      form_set_error('recurring', t('You must specify at least one subscription/payment interval'));
+    }
+  }
+}
+
+/**
+ *
+ */
+function uc_recurring_subscription_product_form_submit(&$form, &$form_state) {
+  global $user;
+  switch ($form_state['values']['op']) {
+    case 'Delete':
+      node_delete($form_state['values']['product_id']);
+      break;
+    default: // Save
+      if (empty($form_state['values']['product_id'])) {
+        $node = new stdClass();
+        $node->type = variable_get('uc_recurring_subscription_product_class', 'uc_recurring_subscription');
+        $node->uid = $user->uid;
+        $node->status = 1;
+        $new = TRUE;
+        node_save($node);
+      }
+      else {
+        $node = node_load($form_state['values']['product_id']);
+        if ($node->type != variable_get('uc_recurring_subscription_product_class', 'uc_recurring_subscription')) {
+          $node->type = variable_get('uc_recurring_subscription_product_class', 'uc_recurring_subscription');
+        }
+        $new = FALSE;
+      }
+
+      if (!empty($node)) {
+        $node->title = $form_state['values']['title'];
+        $node->body = $form_state['values']['body'];
+        $node->list_price = $node->sell_price = $form_state['values']['recurring'][0]['amount'];
+
+        // Product attribute.
+        $attribute = uc_attribute_load(variable_get('uc_recurring_subscription_attribute', ''), $node->nid, 'product');
+
+        // We are going to delete and recreate the product option and adjustments.
+        uc_attribute_subject_delete($attribute->aid, 'product', $node->nid);
+        uc_attribute_adjustments_delete(array('nid' => $node->nid));
+
+        // Create product attribute without the options.
+        uc_attribute_subject_save($attribute, 'product', $node->nid);
+
+        $subscription = uc_recurring_subscription_load($node->nid);
+        if (empty($subscription)) {
+          $subscription = new stdClass();
+          $subscription->nid = $node->nid;
+          $subscription->new = TRUE;
+        }
+        foreach ($form_state['values']['access'][0] as $access => $value) {
+          $subscription->access[$access] = $value;
+        }
+        $key = $subscription->new == TRUE ? NULL : 'nid';
+        drupal_write_record('uc_recurring_subscription', $subscription, $key);
+
+        // add/update the recurring features
+        $features = array();
+        foreach ($form_state['values']['recurring_intervals'] as $index => $value) {
+          if (!is_numeric($index)) continue;
+
+          $new_feature = FALSE;
+          if (!empty($value['pfid'])) {
+            $product = uc_recurring_fee_product_load($value['pfid']);
+          }
+          else {
+            $product = new stdClass();
+            $product->nid = $node->nid;
+            uc_recurring_product_feature_save($product); // We need a new pfid
+          }
+
+          $product->fee_amount = $value['amount'];
+          $product->initial_charge = $value['interval_value'] .' '. $value['interval_unit'];
+          $product->regular_interval = $value['interval_value'] .' '. $value['interval_unit'];
+          // If number intervals is negative, it means that it's unlimited intervals.
+          $product->number_intervals = !empty($value['number_intervals']) ? $value['number_intervals'] : -1;
+
+          // Create a model that hopefully will be unique.
+          $model = 'sub-'. $node->nid .'-'. $product->pfid;
+
+          // get option or create if it doesn't exist.
+          $num = $value['interval_value'];
+          $unit = $value['interval_unit'];
+          $option_name = _uc_recurring_subscription_create_attribute_option($num, $unit);
+          $option = _uc_recurring_subscription_attribute_option_by_name($attribute->aid, $option_name);
+
+          // Set the product option.
+          $option->nid = $node->nid;
+          $option->oid = $option->oid;
+          $option->ordering = $value['weight'];
+          $option->price = $value['amount'];
+          $attribute->options[$option->oid] = $option;
+
+          // Set the default product options.
+          if ($index == $form_state['values']['default_payment']) {
+            $attribute->default_option = $option->oid;
+            $node->model = $model;
+            $node->sell_price = $product->fee_amount;
+          }
+
+          // Set the product adjustments.
+          $adj = new stdClass();
+          $adj->nid = $node->nid;
+          $adj->combination = serialize(array($attribute->aid => $option->oid));
+          $adj->model = $model;
+          drupal_write_record('uc_product_adjustments', $adj);
+
+          // If the model has changed we save the feature again.
+          if ($model != $product->model) {
+            $product->model = $model;
+          }
+          $features[$product->pfid] = $product;
+        }
+
+        // If there is only one option then don't make selection required.
+        $attribute->required = (count($form_state['values']['recurring_intervals']) <= 1) ? 0 : 1;
+
+        // Process trial information.
+        $trial_amount = $form_state['values']['trial_amount'];
+        $trial_value = intval($form_state['values']['trial_interval_value']);
+        $trial_unit = $form_state['values']['trial_interval_unit'];
+        if ($trial_value > 0) {
+          $node->sell_price = $trial_amount;
+        }
+
+        // We need to calculate the option prices adjustments.
+        foreach ($attribute->options as $index => $option) {
+          if ($trial_value > 0) {
+            $attribute->options[$index]->price = 0;
+          }
+          else {
+            $attribute->options[$index]->price = $option->price - $node->sell_price;
+          }
+        }
+        // Save the product attribute and options.
+        uc_attribute_subject_save($attribute, 'product', $node->nid, TRUE);
+        // Save the product features.
+        foreach ($features as $feature) {
+          if ($trial_value > 0) {
+            $feature->initial_charge = $trial_value .' '. $trial_unit;
+          }
+          uc_recurring_product_feature_save($feature);
+        }
+        // Save the product/node.
+        node_save($node);
+      }
+      break;
+  }
+  $form_state['redirect'] = 'admin/store/subscriptions';
+}
+
+/**
+ *
+ */
+function uc_recurring_subscription_subscriber_list($form_state, $product_id = -1) {
+  $form = array();
+
+  $header = array(t('Product'), t('Subscribers'), '');
+
+  $result = db_query("SELECT count(u.uid) AS num_users, n.title, n.nid
+                      FROM {uc_recurring_users} u
+                        LEFT JOIN {uc_recurring_products} p ON p.pfid = u.pfid
+                        LEFT JOIN {uc_recurring_subscription} s ON s.nid = p.nid
+                        LEFT JOIN {node} n ON n.nid = p.nid
+                      WHERE u.remaining_intervals <> 0
+                      GROUP BY n.nid");
+
+  while ($subscribers = db_fetch_object($result)) {
+    if (!isset($subscribers->title)) {
+      $subscribers->title = t('Unknown product');
+    }
+    $row = array(
+      'title' => $subscribers->title,
+      'count' => $subscribers->num_users,
+      'ops' => isset($subscribers->nid) ? l('subscribers', 'admin/store/subscriptions/subscribers/'. $subscribers->nid) : '',
+    );
+    $rows[] = $row;
+  }
+  if (count($rows) <= 0) {
+    $rows[] = array(
+      array(
+        'data' => t('No subscribers to any subscription products.'),
+        'colspan' => count($header),
+      ),
+    );
+  }
+  $form['list'] = array(
+    '#value' => theme('table', $header, $rows),
+  );
+
+  if (is_numeric(arg(4))) {
+    $header = array(
+      array('data' => t('User'), 'field' => 'u.name'),
+      array('data' => t('Subscription Started'), 'field' => 'ru.created'),
+      array('data' => t('Next Renewal'), 'field' => 'ru.next_charge'),
+      array('data' => t('Price'))
+    );
+    $sql = "SELECT u.*, ru.*
+            FROM {users} u
+              LEFT JOIN {uc_recurring_users} ru ON ru.uid = u.uid
+              LEFT JOIN {uc_recurring_products} p ON p.pfid = ru.pfid
+              LEFT JOIN {uc_recurring_subscription} s ON s.nid = p.nid
+              LEFT JOIN {node} n ON n.nid = p.nid
+            WHERE ru.remaining_intervals <> 0 AND n.nid = %d";
+
+    $sql .= tablesort_sql($header);
+    $result = pager_query($sql, 20, 0, NULL, arg(4));
+
+    while ($account = db_fetch_object($result)) {
+      $user_rows[] = array(
+        'user' => $account->name,
+        'start' => format_date($account->created),
+        'next' => format_date($account->next_charge),
+        'price' => uc_price($account->fee_amount, array()),
+        //'status' => uc_recurring_state($account->status),
+      );
+    }
+
+    $form['users'] = array(
+      '#value' => theme('table', $header, $user_rows) . theme('pager', NULL, 20, 0),
+    );
+  }
+
+  return $form;
+}
+
+/**
+ *
+ */
+function uc_recurring_subscription_settings_form($form_state) {
+  $form = array();
+
+  $options = array();
+  $result = db_query("SELECT * FROM {uc_product_classes}");
+  while ($class = db_fetch_array($result)) {
+    $options[$class['pcid']] = $class['name'];
+  }
+  $form['uc_recurring_subscription_product_class'] = array(
+    '#type' => 'select',
+    '#title' => 'Product Class',
+    '#description' => 'Only products from this class will be managed as subscription.',
+    '#options' => $options,
+    '#default_value' => variable_get('uc_recurring_subscription_product_class', 'uc_recurring_subscription'),
+  );
+
+  $options = array();
+  $attributes = uc_class_get_attributes(variable_get('uc_recurring_subscription_product_class', 'uc_recurring_subscription'));
+  foreach($attributes as $id => $value) {
+    $options[$id] = $value->name;
+  }
+  $form['uc_recurring_subscription_attribute'] = array(
+    '#type' => 'select',
+    '#title' => 'Payment Attribute',
+    '#description' => 'The attribute used to provide the payment options to users.',
+    '#options' => $options,
+    '#default_value' => variable_get('uc_recurring_subscription_attribute', ''),
+  );
+
+  return system_settings_form($form);
+}
+
+/**
+ *
+ */
+function theme_uc_recurring_subscription_item($form) {
+  $header = array(t('Default'), t('Subscription Interval'), t('Amount'), t('Billing Intervals'), t('Order'), '');
+  drupal_add_tabledrag('recurring-subscription', 'order', 'sibling', 'recurring-weight');
+
+  foreach (element_children($form) as $key) {
+    if ($key === 'default_payment') {
+      continue;
+    }
+    // Add class to group weight fields for drag and drop.
+    $form[$key]['weight']['#attributes']['class'] = 'recurring-weight';
+
+    $row = array();
+    $row[] = drupal_render($form['default_payment'][$key]);
+    $row[] = drupal_render($form[$key]['interval_value']) .
+             drupal_render($form[$key]['interval_unit']);
+    $row[] = drupal_render($form[$key]['amount']);
+    $row[] = drupal_render($form[$key]['number_intervals']) .
+             drupal_render($form[$key]['unlimited']);
+    $row[] = drupal_render($form[$key]['weight']);
+    $row[] = drupal_render($form[$key]['operations']) . drupal_render($form[$key]['pfid']);
+    $rows[] = array('data' => $row, 'class' => 'draggable tabledrag-leaf');
+  }
+  $output = theme('table', $header, $rows, array('id' => 'recurring-subscription'));
+  //$output .= drupal_render($form);
+  return $output;
+}
+
+/**
+ * Add table drag effect to product commission table.
+ */
+function theme_uc_recurring_subscription_products($form) {
+  $header = array(t('Name'), t('Assigned role(s)'), t('Trial'), t('Payment Options'), '');
+  drupal_add_tabledrag('recurring-subscription-products', 'order', 'sibling', 'product-weight');
+
+  foreach (element_children($form) as $key) {
+    // Add class to group weight fields for drag and drop.
+    $form[$key]['weight']['#attributes']['class'] = 'product-weight';
+
+    $row = array();
+    $row[] = drupal_render($form[$key]['title']);
+    $row[] = drupal_render($form[$key]['role']);
+    $row[] = drupal_render($form[$key]['trial']);
+    $row[] = drupal_render($form[$key]['interval']);
+    $row[] = drupal_render($form[$key]['weight']);
+    $row[] = drupal_render($form[$key]['operations']);
+    $rows[] = array('data' => $row, 'class' => 'draggable');
+  }
+
+  if (empty($rows)) {
+    $rows[] = array(
+      array(
+        'data' => l('Add new subscription', 'admin/store/subscriptions/create'),
+        'colspan' => 5,
+      ),
+    );
+  }
+
+  $output = theme('table', $header, $rows, array('id' => 'recurring-subscription-products'));
+  $output .= '<div><span class="default-option">'. t('default subscription option') .'</span></div>';
+  $output .= drupal_render($form);
+  return $output;
+}
+
+/**
+ *
+ */
+function theme_uc_recurring_subscription_role_items($form) {
+  $header = array(t('Granted on purchase'), t('Granted on expiration'), t('Revoked on expiration'));
+
+  $row = array();
+  $row[] = drupal_render($form['subscribe_grant']);
+  $row[] = drupal_render($form['expire_grant']);
+  $row[] = drupal_render($form['expire_revoke']);
+  $rows[] = $row;
+
+  $output = theme('table', $header, $rows);
+  $output .= drupal_render($form);
+  return $output;
+}
+
+/**
+ *
+ */
+function theme_uc_recurring_subscription_og_items($form) {
+  $header = array(t('Subscribe on purchase'), t('Unsubscribe on expiration'));
+
+  $row = array();
+  $row[] = drupal_render($form['subscribe_gid']);
+  $row[] = drupal_render($form['unsubscribe_gid']);
+  $rows[] = $row;
+
+  $output = theme('table', $header, $rows);
+  $output .= drupal_render($form);
+  return $output;
+}
diff --git modules/uc_recurring_subscription/uc_recurring_subscription.ca.inc modules/uc_recurring_subscription/uc_recurring_subscription.ca.inc
new file mode 100644
index 0000000..096c6af
--- /dev/null
+++ modules/uc_recurring_subscription/uc_recurring_subscription.ca.inc
@@ -0,0 +1,283 @@
+<?php
+// $Id$
+
+/**
+ * Implementation of hook_ca_entity().
+ */
+function uc_recurring_subscription_ca_entity() {
+  $entities = array();
+
+  $entities['uc_recurring_subscription'] = array(
+    '#title' => t('Recurring subscription'),
+    '#type' => 'object',
+  );
+
+  return $entities;
+}
+
+/**
+ * Implementation of hook_ca_condition().
+ */
+function uc_recurring_subscription_ca_condition() {
+  $condition = array();
+  return $conditions;
+}
+
+/**
+ * Implementation of hook_ca_action().
+ */
+function uc_recurring_subscription_ca_action() {
+  // actions to grant/revoke role on user account.
+  $actions['uc_recurring_subscription_grant_role'] = array(
+    '#title' => t('Grant a role on an order.'),
+    '#category' => t('Role'),
+    '#callback' => 'uc_recurring_subscription_action_grant_role',
+    '#arguments' => array(
+      'order' => array(
+        '#entity' => 'uc_order',
+        '#title' => t('Order'),
+      ),
+    ),
+  );
+  $actions['uc_recurring_subscription_revoke_role'] = array(
+    '#title' => t('Revoke a role on an order.'),
+    '#category' => t('Role'),
+    '#callback' => 'uc_recurring_subscription_action_revoke_role',
+    '#arguments' => array(
+      'order' => array(
+        '#entity' => 'uc_order',
+        '#title' => t('Order'),
+      ),
+    ),
+  );
+
+  if (module_exists('uc_og_subscribe')) {
+    // actions to subscribe/unsubscribe user from organic group.
+    $actions['uc_recurring_subscription_subscribe_og'] = array(
+      '#title' => t('Subscribe user to an organic group.'),
+      '#category' => t('Organic Group'),
+      '#callback' => 'uc_recurring_subscription_action_subscribe_og',
+      '#arguments' => array(
+        'order' => array(
+          '#entity' => 'uc_order',
+          '#title' => t('Order'),
+        ),
+      ),
+    );
+    $actions['uc_recurring_subscription_unsubscribe_og'] = array(
+      '#title' => t('Unsubscribe user from organic group.'),
+      '#category' => t('Organic Group'),
+      '#callback' => 'uc_recurring_subscription_action_unsubscribe_role',
+      '#arguments' => array(
+        'order' => array(
+          '#entity' => 'uc_order',
+          '#title' => t('Order'),
+        ),
+      ),
+    );
+  }
+  return $actions;
+}
+
+/**
+ * Implementation of hook_ca_trigger().
+ */
+function uc_recurring_subscription_ca_trigger() {
+  // use the existing uc_recurring triggers
+  $triggers = array();
+  return $triggers;
+}
+
+/**
+ * Implementation of hook_ca_predicate().
+ */
+function uc_recurring_subscription_ca_predicate() {
+  $predicates = array();
+
+  // Grant user roles to user when checkout is complete.
+  $predicates['uc_recurring_subscription_grant_roles'] = array(
+    '#title' => t('Grant user roles on subscription created.'),
+    '#trigger' => 'uc_order_status_update',
+    '#class' => 'roles',
+    '#status' => 1,
+    '#weight' => 1,
+    '#conditions' => array(
+      '#operator' => 'AND',
+      '#conditions' => array(
+        array(
+          '#name' => 'uc_order_status_condition',
+          '#title' => t('If order status is completes.'),
+          '#argument_map' => array(
+            'order' => 'updated_order',
+          ),
+          '#settings' => array(
+            'negate' => FALSE,
+            'order_status' => 'completed',
+          ),
+        ),
+      ),
+    ),
+    '#actions' => array(
+      array(
+        '#name' => 'uc_recurring_subscription_grant_role',
+        '#title' => t('Grant user role(s).'),
+        '#argument_map' => array(
+          'order' => 'order',
+        ),
+        '#settings' => array(
+          'role_option' => 'subscribe_grant',
+          'role' => array(),
+        ),
+      )
+    ),
+  );
+
+  // Grant user roles to user when subscription expires.
+  $predicates['uc_recurring_subscription_expired_grant_roles'] = array(
+    '#title' => t('Grant user roles on subscription expiration.'),
+    '#trigger' => 'uc_recurring_renewal_expired',
+    '#class' => 'roles',
+    '#status' => 1,
+    '#weight' => 1,
+    '#conditions' => array(
+      '#operator' => 'AND',
+      '#conditions' => array(
+      ),
+    ),
+    '#actions' => array(
+      array(
+        '#name' => 'uc_recurring_subscription_grant_role',
+        '#title' => t('Grant user role(s).'),
+        '#argument_map' => array(
+          'order' => 'order',
+        ),
+        '#settings' => array(
+          'role_option' => 'expire_grant',
+          'role' => array(),
+        ),
+      )
+    ),
+  );
+
+  // Revoke user roles to user when subscription expires.
+  $predicates['uc_recurring_subscription_expired_revoke_roles'] = array(
+    '#title' => t('Revoke user roles on subscription expiration.'),
+    '#trigger' => 'uc_recurring_renewal_expired',
+    '#class' => 'roles',
+    '#status' => 1,
+    '#weight' => 1,
+    '#conditions' => array(
+      '#operator' => 'AND',
+      '#conditions' => array(
+      ),
+    ),
+    '#actions' => array(
+      array(
+        '#name' => 'uc_recurring_subscription_revoke_role',
+        '#title' => t('Grant user role(s).'),
+        '#argument_map' => array(
+          'order' => 'order',
+        ),
+        '#settings' => array(
+          'role_option' => 'expire_revoke',
+          'role' => array(),
+        ),
+      )
+    ),
+  );
+
+
+  return $predicates;
+}
+
+/**
+ * Grant a role.
+ */
+function uc_recurring_subscription_action_grant_role($order, $settings) {
+  $account = user_load($order->uid);
+
+  $roles = user_roles(TRUE);
+  if ($settings['role_option'] == 'custom') {
+    $account->roles += $settings['role'];
+    watchdog('uc_recurring', 'Granted !role role to !user', array('!role' => $roles[$rid], '!user' => $account->name));
+  }
+  else {
+    foreach ($order->products as $pid => $product) {
+      $subscription = uc_recurring_subscription_load($product->nid);
+      $account->roles += $subscription->access[$settings['role_option']];
+    }
+  }
+  user_save($account, array('roles' => $account->roles));
+}
+
+/**
+ * Grant role CA form.
+ */
+function uc_recurring_subscription_action_grant_role_form($form_state, $settings = array()) {
+  drupal_add_css(drupal_get_path('module', 'uc_recurring_subscription') .'/uc_recurring_subscription.css');
+  $form['role_option'] = array(
+    '#type' => 'radios',
+    '#title' => t('Where are the role(s) defined?'),
+    '#attributes' => array('class' => 'ca-role-select-option'),
+    '#options' => array(
+      'subscribe_grant' => t('Apply the roles set by the Subscription Manager on creation.'),
+      'expire_grant' => t('Apply the roles set by the Subscription Manager on expiration.'),
+      'expire_revoke' => t('Revoke the roles set by the Subscription Manager on expiration.'),
+      'custom' => t('Custom selected role (select from below)'),
+    ),
+    '#default_value' => $settings['role_option'],
+  );
+
+  $roles = user_roles(TRUE);
+  unset($roles[DRUPAL_AUTHENTICATED_RID]);
+  if (empty($roles)) {
+    $form['role_error'] = array(
+      '#value' => t('There are no roles available to select. <a href="!url">Add a role</a>.', array('!url' => url('admin/user/roles'))),
+    );
+  }
+  else {
+    $form['role'] = array(
+      '#type' => 'checkboxes',
+      '#title' => t('Custom roles'),
+      '#attributes' => array('class' => 'ca-role-option'),
+      '#options' => $roles,
+      '#default_value' => $settings['role'],
+    );
+  }
+  return $form;
+}
+
+/**
+ * Revoke user role(s).
+ */
+function uc_recurring_subscription_action_revoke_role($order, $settings) {
+  $account = user_load($order->uid);
+
+  $roles = user_roles(TRUE);
+  if ($settings['role_option'] == 'custom') {
+    foreach ($settings['role'] as $rid => $role) {
+      unset($account->roles[$rid]);
+      watchdog('uc_recurring', 'Revoked !role role from !user', array('!role' => $roles[$rid], '!user' => $account->name));
+    }
+  }
+  else {
+    foreach ($order->products as $pid => $product) {
+      $subscription = uc_recurring_subscription_load($product->nid);
+      foreach ($subscription->roles[$settings['role_option']] as $rid => $role) {
+        // @todo: we need to check other subscriptions to ensure that the role
+        // is no longer required by another subscription.
+        unset($account->roles[$rid]);
+      }
+    }
+  }
+  user_save($account, array('roles' => $account->roles));
+
+  $account = user_load($order->uid);
+}
+
+/**
+ * The revoke form is the same as the grant role form.
+ */
+function uc_recurring_subscription_action_revoke_role_form($form_state, $settings = array()) {
+  return uc_recurring_subscription_action_grant_role_form($form_state, $settings);
+}
diff --git modules/uc_recurring_subscription/uc_recurring_subscription.css modules/uc_recurring_subscription/uc_recurring_subscription.css
new file mode 100644
index 0000000..8078930
--- /dev/null
+++ modules/uc_recurring_subscription/uc_recurring_subscription.css
@@ -0,0 +1,28 @@
+div#recurring_intervals {
+  clear: both;
+}
+
+#edit-notication-event-wrapper label {
+  display: inline;
+}
+
+form#uc-recurring-subscription-product-form .subscription-interval-value,
+form#uc-recurring-subscription-product-form .subscription-interval-period,
+form#uc-recurring-subscription-product-form .subscription-num-intervals,
+form#uc-recurring-subscription-product-form .subscription-unlimited-intervals {
+  float: left;
+}
+
+
+form#uc-recurring-subscription-overview span.default-option:before {
+  content: "*";
+}
+
+.ca-role-option div.form-item {
+  display: inline;
+  padding-right: 20px;
+}
+
+.role-option {
+  width: 100%;
+}
diff --git modules/uc_recurring_subscription/uc_recurring_subscription.info modules/uc_recurring_subscription/uc_recurring_subscription.info
new file mode 100644
index 0000000..c357fee
--- /dev/null
+++ modules/uc_recurring_subscription/uc_recurring_subscription.info
@@ -0,0 +1,8 @@
+; $Id:$
+name = Subscription Manager
+description = Provides a Subscription Manager UI for paid membership features.
+dependencies[] = uc_recurring
+dependencies[] = uc_attribute
+package = Ubercart - recurring
+core = 6.x
+php = 5.0
diff --git modules/uc_recurring_subscription/uc_recurring_subscription.install modules/uc_recurring_subscription/uc_recurring_subscription.install
new file mode 100644
index 0000000..d581b35
--- /dev/null
+++ modules/uc_recurring_subscription/uc_recurring_subscription.install
@@ -0,0 +1,127 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Installs the Recurring Subscription module.
+ */
+
+/**
+ * Implementation of hook_requirements().
+ */
+function uc_recurring_subscription_requirements($phase) {
+  $requirements = array();
+  // Ensure translations don't break at install time.
+  $t = get_t();
+
+  if ($phase == 'runtime') {
+    if (!function_exists('uc_attribute_load_multiple')) {
+      $requirements['uc_recurring_subscription'] = array(
+        'title' => $t('Subscription Manager'),
+        'value' => $t('Patch required to Ubercart.'),
+        'severity' => REQUIREMENT_ERROR,
+        'description' => $t('This module requires a patch to ubercart, read the <a href="@readme">README.txt</a>', array('@readme' => url(drupal_get_path('module', 'uc_recurring_subscription') .'/README.txt'))),
+      );
+    }
+  }
+  return $requirements;
+}
+
+/**
+ * Implementation hook_schema().
+ */
+function uc_recurring_subscription_schema() {
+  $schema = array();
+
+  $schema['uc_recurring_subscription'] = array(
+    'description' => t('Data for recurring fees attached to products.'),
+    'fields' => array(
+      'nid' => array(
+        'description' => t('The product ID.'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'access' => array(
+        'description' => t('Serialized list of drupal roles that should be applied on this subscription.'),
+        'type' => 'text',
+        'serialize' => TRUE,
+      ),
+      'ca' => array(
+        'description' => t('Serialized list of drupal CA events to tie to this subscription.'),
+        'type' => 'text',
+        'serialize' => TRUE,
+      ),
+      'weight' => array(
+        'description' => t('The order the product is listed.'),
+        'type' => 'int',
+        'unsigned' => FALSE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'parent' => array(
+        'description' => t('Allows subscription to inherit roles and notifications.'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => FALSE,
+      ),
+    ),
+    'primary key' => array('nid'),
+  );
+
+  return $schema;
+}
+
+/**
+ * Implementation hook_install().
+ */
+function uc_recurring_subscription_install() {
+  drupal_install_schema('uc_recurring_subscription');
+}
+
+/**
+ * Implementation hook_uninstall().
+ */
+function uc_recurring_subscription_uninstall() {
+  drupal_uninstall_schema('uc_recurring_subscription');
+
+  variable_del('uc_recurring_subscription_product_class');
+  variable_del('uc_recurring_subscription_attribute');
+}
+
+/**
+ * Implementation hook_enable().
+ */
+function uc_recurring_subscription_enable() {
+  $pcid = 'uc_recurring_subscription';
+
+  if (db_result(db_query("SELECT count(*) FROM {uc_product_classes} WHERE pcid = '%s'", $pcid)) < 1) {
+    db_query("INSERT INTO {uc_product_classes} (pcid, name, description) VALUES ('%s', 'Recurring Subscription', 'A recurring subscription product used by the recurring subscription ui module to manage your subscription products')", $pcid);
+    uc_product_node_info(TRUE);
+    node_types_rebuild();
+    menu_rebuild();
+  }
+  variable_set('node_options_'. $pcid, variable_get('node_options_product', array('status', 'promote')));
+  variable_set('uc_recurring_subscription_product_class', $pcid);
+
+  $attribute = new stdClass();
+  $attribute->name = 'uc_recurring_subscription_payment_options';
+  $attribute->label = 'Payment Option';
+  $attribute->ordering = 0;
+  $attribute->required = 1;
+  $attribute->display = 1;
+  $attribute->description = 'The subscription payment options';
+  uc_attribute_save(&$attribute);
+  uc_attribute_subject_save($attribute, 'class', 'uc_recurring_subscription');
+  variable_set('uc_recurring_subscription_attribute', $attribute->aid);
+}
+
+/**
+ *
+ */
+function uc_recurring_subscription_update_6000() {
+  $ret = array();
+  db_change_field($ret, 'uc_recurring_subscription', 'roles', 'access', array('type' => 'text', 'serialize' => TRUE));
+  return $ret;
+}
diff --git modules/uc_recurring_subscription/uc_recurring_subscription.js modules/uc_recurring_subscription/uc_recurring_subscription.js
new file mode 100644
index 0000000..17bb0f2
--- /dev/null
+++ modules/uc_recurring_subscription/uc_recurring_subscription.js
@@ -0,0 +1,17 @@
+/**
+ * Disable and enable fields in the recurring subscription form.
+ */
+Drupal.behaviors.UcRecurringSubscriptionForm = function (context) {
+  $(".unlimited-checkbox").change(function() {
+    var id = $(this).attr('id');
+    text_id = id.replace('unlimited', 'number-intervals');
+    if ($("#"+id).attr("checked")) {
+      $("#"+text_id).attr("disabled","disabled");
+      $("#"+text_id).val('');
+    }
+    else {
+      $("#"+text_id).removeAttr("disabled");
+    }
+  });
+};
+
diff --git modules/uc_recurring_subscription/uc_recurring_subscription.module modules/uc_recurring_subscription/uc_recurring_subscription.module
new file mode 100644
index 0000000..0eed2eb
--- /dev/null
+++ modules/uc_recurring_subscription/uc_recurring_subscription.module
@@ -0,0 +1,277 @@
+<?php
+// $Id:$
+
+/**
+ * @file
+ * Uc recurring subscription UI.
+ */
+
+/**
+ * Implementation of hook_help().
+ */
+function uc_recurring_subscription_help($path, $arg) {
+  switch ($path) {
+    case 'admin/store/subscriptions':
+    case 'admin/store/subscriptions/overview':
+      return t("Below is a list of the subscription products managed by the subscription manager. A subscription is a product that members can purchase to gain access to a role on the site that is automatically renewed on a specific interval. You can add new subscription using the create subscription tab.");
+  }
+}
+
+/**
+ * Implementation of hook_init().
+ */
+function uc_recurring_subscription_init() {
+  module_load_include('inc', 'uc_recurring_subscription', 'uc_recurring_subscription.ca');
+}
+
+/**
+ * Implementation of hook_perm().
+ */
+function uc_recurring_subscription_perm() {
+  return array('manage subscriptions');
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function uc_recurring_subscription_menu() {
+  $items = array();
+
+  $items['admin/store/subscriptions'] = array(
+    'title' => 'Subscription Manager',
+    'description' => 'Manage recurring subscription products.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('uc_recurring_subscription_overview'),
+    'access arguments' => array('manage subscriptions'),
+    'type' => MENU_NORMAL_ITEM,
+    'file' => 'uc_recurring_subscription.admin.inc',
+  );
+  $items['admin/store/subscriptions/overview'] = array(
+    'title' => 'Overview',
+    'weight' => -10,
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+  );
+  $items['admin/store/subscriptions/create'] = array(
+    'title' => 'Create Subscription',
+    'description' => '.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('uc_recurring_subscription_product_form'),
+    'access arguments' => array('manage subscriptions'),
+    'type' => MENU_LOCAL_TASK,
+    'file' => 'uc_recurring_subscription.admin.inc',
+  );
+  $items['admin/store/subscriptions/%/edit'] = array(
+    'title' => 'Edit Subscription',
+    'description' => '.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('uc_recurring_subscription_product_form', 3),
+    'access arguments' => array('manage subscriptions'),
+    'type' => MENU_CALLBACK,
+    'file' => 'uc_recurring_subscription.admin.inc',
+  );
+  $items['node/%/edit/subscription'] = array(
+    'title' => 'Subscription',
+    'description' => '.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('uc_recurring_subscription_product_form', 1),
+    'access callback' => 'uc_product_edit_access',
+    'access arguments' => array(1),
+    'type' => MENU_LOCAL_TASK,
+    'file' => 'uc_recurring_subscription.admin.inc',
+  );
+  $items['admin/store/subscriptions/subscribers'] = array(
+    'title' => 'Manage Subscribers',
+    'description' => '.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('uc_recurring_subscription_subscriber_list'),
+    'access arguments' => array('manage subscriptions'),
+    'type' => MENU_LOCAL_TASK,
+    'file' => 'uc_recurring_subscription.admin.inc',
+  );
+  $items['admin/store/subscriptions/settings'] = array(
+    'title' => 'Subscription Settings',
+    'description' => '.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('uc_recurring_subscription_settings_form'),
+    'access arguments' => array('manage subscriptions'),
+    'type' => MENU_LOCAL_TASK,
+    'file' => 'uc_recurring_subscription.admin.inc',
+  );
+  $items['subscriptions/ahah/%'] = array(
+    'page callback' => 'uc_recurring_subscription_ahah',
+    'page arguments' => array(2),
+    'access arguments' => array('manage subscriptions'),
+    'type' => MENU_CALLBACK,
+    'file' => 'uc_recurring_subscription.admin.inc',
+  );
+
+  // override the ability to delete the subscription attribute
+  $items['admin/store/attributes/'. variable_get('uc_recurring_subscription_attribute', '') .'/delete'] = array(
+    'title' => 'Delete operation not Availabe',
+    'page callback' => 'uc_recurring_subscription_attribute_delete',
+    'access arguments' => array('administer attributes'),
+    'type' => MENU_CALLBACK,
+  );
+
+  return $items;
+}
+
+/**
+ *
+ */
+function uc_recurring_subscription_menu_alter(&$items) {
+}
+
+/**
+ *
+ */
+function uc_recurring_subscription_attribute_delete() {
+  return t('This attribute cannot be deleted as it is used by the subscrition products to provide payment options.');
+}
+
+/**
+ * Load a recurring subscription object.
+ */
+function uc_recurring_subscription_load($nid) {
+  $subscription = db_fetch_object(db_query("SELECT * FROM {uc_recurring_subscription} WHERE nid = %d", $nid));
+  if (!empty($subscription)) {
+    $subscription->access = empty($subscription->access) ? array() : unserialize($subscription->access);
+  }
+  return $subscription;
+}
+
+/**
+ * Delete recurring subscription.
+ */
+function uc_recurring_subscription_delete($nid) {
+  db_query("DELETE FROM {uc_recurring_subcription} WHERE nid = %d", $nid);
+}
+
+/**
+ * For recurring subscriptions we should.
+ */
+function uc_recurring_subscription_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
+  if ($node->type == variable_get('uc_recurring_subscription_product_class', 'uc_recurring_subscription')) {
+    switch ($op) {
+      case 'load':
+        $node->subscription = uc_recurring_subscription_load($node->nid);
+    }
+  }
+}
+
+/**
+ * This loads the reucrring product features as well as the attribute option for
+ * this feature.
+ */
+function _uc_recurring_subscription_get_product_features($product_id) {
+  $attribute = uc_attribute_load(variable_get('uc_recurring_subscription_attribute', ''), $product_id, 'product');
+
+  $result = db_query("SELECT pfid from {uc_recurring_products} WHERE nid = '%d'", $product_id);
+  while ($p = db_fetch_object($result)) {
+    $product = uc_recurring_fee_product_load($p->pfid);
+
+    $option_name = _uc_recurring_subscription_create_attribute_option($product->regular_interval_value, $product->regular_interval_unit);
+    $option = _uc_recurring_subscription_option($option_name);
+    $product->option = uc_attribute_subject_option_load($option->oid, 'product', $product_id);
+
+    if ($attribute->default_option == $option->oid) {
+      $product->option->default_option = TRUE;
+    }
+
+    $products[] = $product;
+  }
+  if (count($products) > 1) {
+    usort($products, '_uc_recurring_subscription_product_feature_sort');
+  }
+  return $products;
+}
+
+/**
+ *
+ */
+function _uc_recurring_subscription_option($option_name) {
+  $attribute = uc_attribute_load(variable_get('uc_recurring_subscription_attribute', ''));
+  $option = _uc_recurring_subscription_attribute_option_by_name($attribute->aid, $option_name);
+  return $option;
+}
+
+/**
+ * Implementation of hook_theme()
+ */
+function uc_recurring_subscription_theme() {
+  return array(
+    'uc_recurring_subscription_item' => array(
+      'arguments' => array('form' => NULL),
+    ),
+    'uc_recurring_subscription_products' => array(
+      'arguments' => array('form' => NULL),
+    ),
+    'uc_recurring_subscription_role_items' => array(
+      'arguments' => array('form' => NULL),
+    ),
+    'uc_recurring_subscription_og_items' => array(
+      'arguments' => array('form' => NULL),
+    ),
+  );
+}
+
+/**
+ * Format a standard naming convension for attribute names.
+ */
+function _uc_recurring_subscription_create_attribute_option($value, $interval) {
+  if ($value == 1) {
+    $intervals = array(
+      'days' => t('Daily'),
+      'weeks' => t('Weekly'),
+      'months' => t('Monthly'),
+      'year' => t('Yearly')
+    );
+    return $intervals[$interval];
+  }
+  return t('Every @value @interval', array('@value' => $value, '@interval' => $interval));
+}
+
+/**
+ * Find attribute option with a given aid and name
+ */
+function _uc_recurring_subscription_attribute_option_by_name($aid, $name) {
+  $option = db_fetch_object(db_query("SELECT * from {uc_attribute_options} WHERE aid = %d AND name = '%s'", $aid, $name));
+  // If it doesn't exist we create it.
+  if (empty($option)) {
+    $option = new stdClass();
+    $option->aid = $aid;
+    $option->name = $name;
+    uc_attribute_option_save($option);
+  }
+  return $option;
+}
+
+/**
+ * Function used by uasort to sort product features.
+ */
+function _uc_recurring_subscription_product_feature_sort($a, $b) {
+  $a_weight = (isset($a->option) && isset($a->option->ordering)) ? $a->option->ordering : 0;
+  $b_weight = (isset($b->option) && isset($b->option->ordering)) ? $b->option->ordering : 0;
+  if ($a_weight == $b_weight) {
+    return 0;
+  }
+  return ($a_weight < $b_weight) ? -1 : 1;
+}
+
+/**
+ * Implementation of hook_form_alter().
+ */
+function uc_recurring_subscription_form_alter(&$form, $form_state, $form_id) {
+  $aid = variable_get('uc_recurring_subscription_attribute', '');
+  if (isset($form['aid']) && $form['aid']['#value'] == $aid) {
+    // Hide critical options from forum vocabulary.
+    if ($form_id == 'uc_attribute_form') {
+      $form['help_subscription_attribute'] = array(
+        '#value' => t('This is the designated subscription product attribute. Some of the normal attribute options have been disabled.'),
+        '#weight' => -1,
+      );
+      $form['name']['#disabled'] = TRUE;
+    }
+
+  }
+}
