Skip to content
uc_recurring.module 38.1 KiB
Newer Older
<?php

/**
 * @file
 * Allows you to add a recurring fee to a product/SKU to handle subscription
 *
 * This module includes code for the recurring fee product feature and a default
 * recurring fee handler.  The default handler simply adds fees to the queue to
 * be processed on cron runs.
 * Initial charges, even if they're set to occur in 0 days will not be processed
 * immediately upon checkout
 *
 * Development
 *   Chris Hood http://univate.com.au
/**
 * Shortcuts for defining disabled or default menu items in the recurring info
 * definitions, rather then needing to specify the whole menu item.
 * UC_RECURRING_MENU_DISABLED is included for completeness, menu items are
 * assumed to be disabled if not included in a recurring definition.
 *
 * @see hook_recurring_info()
 */
define('UC_RECURRING_MENU_DISABLED', 0);
define('UC_RECURRING_MENU_DEFAULT', 1);
/**
 * Recurring fee states.
 */
define('UC_RECURRING_FEE_STATUS_ACTIVE', 0);
define('UC_RECURRING_FEE_STATUS_EXPIRED', 1);
define('UC_RECURRING_FEE_STATUS_CANCELLED', 2);
univate's avatar
univate committed
define('UC_RECURRING_FEE_STATUS_SUSPENDED', 3);
/**
 * Recurring access.
 */
define('UC_RECURRING_ACCESS_DENY', 'deny');
define('UC_RECURRING_ACCESS_ALLOW', 'allow');
define('UC_RECURRING_ACCESS_IGNORE', NULL);

/**
 * Unlimited number of intervals.
 */
define('UC_RECURRING_UNLIMITED_INTERVALS', -1);

/*******************************************************************************
 * Drupal Hooks
 ******************************************************************************/

univate's avatar
univate committed
 * Implements hook_menu().
 */
function uc_recurring_menu() {
  $items = array();

  $items['admin/store/settings/payment/edit/recurring'] = array(
    'title' => 'Recurring payments',
    'description' => 'Edit the recurring payment settings.',
    'access arguments' => array('administer store'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('uc_recurring_payment_form'),
    'weight' => 0,
    'type' => MENU_LOCAL_TASK,
    'file' => 'uc_recurring.admin.inc',
  );

  $items['admin/store/orders/recurring'] = array(
    'title' => 'Recurring fees',
    'description' => 'View the recurring fees on your orders.',
    'page callback' => 'uc_recurring_admin',
    'page arguments' => array(NULL, NULL),
    'access arguments' => array('administer recurring fees'),
    'type' => MENU_NORMAL_ITEM,
    'weight' => 5,
    'file' => 'uc_recurring.admin.inc',
  );
  $items['admin/store/orders/recurring/view/fee/%'] = array(
    'title' => 'Recurring fees',
    'description' => 'View a specific recurring fee.',
    'page callback' => 'uc_recurring_admin',
    'access arguments' => array('administer recurring fees'),
    'weight' => 5,
    'file' => 'uc_recurring.admin.inc',
  );
  $items['admin/store/orders/recurring/view/order/%'] = array(
    'title' => 'Recurring fees',
    'description' => 'View the recurring fees on a specific order.',
    'page callback' => 'uc_recurring_admin',
    'access arguments' => array('administer recurring fees'),
    'weight' => 5,
    'file' => 'uc_recurring.admin.inc',
  );
  $items['user/%user/recurring-fees'] = array(
    'title' => 'Recurring fees',
    'description' => 'View current recurring fees.',
    'page callback' => 'uc_recurring_user_fees',
    'page arguments' => array(1),
    'access callback' => 'uc_recurring_user_access',
    'access arguments' => array(1),
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/store/orders/%/recurring'] = array(
    'title' => 'Recurring fees',
    'description' => 'Recurring fees associated with this order.',
    'page callback' => 'uc_recurring_order_information',
    'page arguments' => array(3),
    'access arguments' => array('administer recurring fees'),
    'type' => MENU_LOCAL_TASK,
    'file' => 'uc_recurring.admin.inc',
  );

  // Recurring fee payment methods and gateways can define their own list of
  // user operations via the same standard menu structure.
  $info = uc_recurring_get_recurring_info();
  foreach ($info as $handler => $value) {
    if (!empty($value['menu'])) {
      foreach ($value['menu'] as $path => $menu_item) {
        if ($menu_item != UC_RECURRING_MENU_DISABLED) {
          if ($menu_item == UC_RECURRING_MENU_DEFAULT) {
            $menu_item = !empty($info['default']['menu'][$path]) ? $info['default']['menu'][$path] : array();
          }
          $default_menu_fields = array(
            'title' => $path,
            'page callback' => 'drupal_get_form',
            'access callback' => 'uc_recurring_user_access',
            'type' => MENU_CALLBACK,
            'file path' => drupal_get_path('module', $value['module']),
          );
univate's avatar
univate committed
          $admin_path = 'admin/store/orders/recurring/%/' . $path . '/' . $value['fee handler'];
          $items[$admin_path] = array_merge($default_menu_fields, $menu_item);
          $items[$admin_path]['page arguments'][] = 4; // Add rfid as an arguments.
          $items[$admin_path]['page arguments'][] = 6; // Add fee_handler as an arguments.

univate's avatar
univate committed
          $user_path = 'user/%user/recurring/%/' . $path . '/' . $value['fee handler'];
          $default_menu_fields['access arguments'] = array(1, 3, 4);
          $items[$user_path] = array_merge($default_menu_fields, $menu_item);
          $items[$user_path]['page arguments'][] = 3; // Add rfid as an arguments.
          $items[$user_path]['page arguments'][] = 5; // Add fee_handler as an arguments.
        }
      }
    }
  }
univate's avatar
univate committed
 * Implements hook_permission().
univate's avatar
univate committed
function uc_recurring_permission() {
  return array(
    'administer recurring fees' => array(
      'title' => t('administer recurring fees'),
      'description' => t('TODO Add a description for \'administer recurring fees\''),
    ),
    'view own recurring fees' => array(
      'title' => t('view own recurring fees'),
      'description' => t('TODO Add a description for \'view own recurring fees\''),
    ),
  );
univate's avatar
univate committed
 * Implements hook_theme().
function uc_recurring_theme() {
  return array(
    'uc_recurring_user_table' => array(
univate's avatar
univate committed
      'variables' => array('uid' => NULL),
    'uc_recurring_admin_table' => array(
univate's avatar
univate committed
      'variables' => array('fees' => NULL),
univate's avatar
univate committed
 * Implements hook_recurring_info().
 * Add a default handler.
 */
function uc_recurring_recurring_info() {
  $items['default'] = array(
    'name' => t('Default handler'),
    'process callback' => 'uc_recurring_default_handler',
    'renew callback' => 'uc_recurring_default_handler',
    'menu' => array(
      'charge' => array(
        'page arguments' => array('uc_recurring_admin_charge_form'),
        'access callback' => 'user_access',
        'access arguments' => array('administer recurring fees'),
        'file' => 'uc_recurring.admin.inc',
        'file path' => drupal_get_path('module', 'uc_recurring'),
        'page arguments' => array('uc_recurring_admin_edit_form'),
        'access callback' => 'user_access',
        'access arguments' => array('administer recurring fees'),
        'file' => 'uc_recurring.admin.inc',
        'file path' => drupal_get_path('module', 'uc_recurring'),
        'page arguments' => array('uc_recurring_user_cancel_form'),
        'file' => 'uc_recurring.pages.inc',
        'file path' => drupal_get_path('module', 'uc_recurring'),
univate's avatar
univate committed
 * Implements hook_cron().
 * On the renewal time of a recurring fee see if the payment method would
 * like to perform any addition actions.
 * TODO: limit the number of recurring orders to process on each cron job (maybe
 * this can be done based on time, in case a remote server is slow a responding
 * to requests).
 */
function uc_recurring_cron() {
  $start = time();
  lock_acquire('uc_recurring_cron', 30);
  if (variable_get('uc_recurring_trigger_renewals', TRUE)) {
    $fees = uc_recurring_get_fees_for_renew();
    if (!empty($fees)) {
        if ($order_id = uc_recurring_renew($fee)) {
          $success++;
        }
        else {
          // payment attempted but failed
          $fail++;
        }
      watchdog('uc_recurring', '@success recurring fees processed successfully; @fail failed.', array('@success' => $success, '@fail' => $fail));

  $fees = uc_recurring_get_fees_for_expiration();
  if (!empty($fees)) {
      uc_recurring_expire($fee);
    }
    watchdog('uc_recurring', '@count recurring fees expired successfully.', array('@count' => count($fees)));
  }
univate's avatar
univate committed
 * Implements hook_user_view().
univate's avatar
univate committed
function uc_recurring_user_view($account, $view_mode) {
  // Show only if user is privileged, and there are recurring fees.
  if (uc_recurring_user_access($account)) {
    $account->content['summary']['recurring_fees'] = array(
      '#type' => 'user_profile_item',
      '#title' => t('Recurring fees'),
      '#markup' => l('Click here to view your recurring fees', 'user/' . $account->uid . '/recurring-fees'),
    );
 */
function uc_recurring_user_fees($user) {
univate's avatar
univate committed
  return theme('uc_recurring_user_table', array('uid' => $user->uid));

/*******************************************************************************
 * Ubercart Hooks
 ******************************************************************************/

/**
univate's avatar
univate committed
 * Implements hook_uc_order().
function uc_recurring_uc_order($op, $arg1, $arg2) {
    case 'delete':
      $fees = uc_recurring_get_fees($arg1);
      foreach ($fees as $fee) {
        uc_recurring_fee_cancel($fee->rfid, $fee);
        uc_recurring_fee_user_delete($fee->rfid);
univate's avatar
univate committed
 * Implements hook_uc_checkout_complete().
 *
 * This function is require to complete recurring orders for anonymous
 * purchases as the uid is not set until the order has completed checkout.
 */
function uc_recurring_uc_checkout_complete($order, $account) {
  $fees = uc_recurring_get_fees($order);
  foreach ($fees as $fee) {
    if ($fee->uid == 0) { // check for anonymous uid
      $fee->uid = $order->uid;
      uc_recurring_fee_user_save($fee);
    }
  }
}

univate's avatar
univate committed
 * Implements hook_uc_message().
 */
function uc_recurring_uc_message() {
  $messages['uc_recurring_renewal_completed_subject'] = t('[store-name]: [fee-title]');
  $messages['uc_recurring_renewal_completed_message'] = t("[order-first-name] [order-last-name], \n\n[fee-title] has been processed, [order-link], at [store-name].\n\nThanks again, \n\n[store-name]\n[site-slogan]");

  $messages['uc_recurring_renewal_failed_subject'] = t('[store-name]: [fee-title] failed');
  $messages['uc_recurring_renewal_failed_message'] = t("[order-first-name] [order-last-name], \n\nWe have failed to process [fee-title], at [store-name].\n\nWe will re-attempt to process this fee again on [next-charge]. If your payment details have changed you can update them at [recurring-link].\n\nThanks again, \n\n[store-name]\n[site-slogan]");

  $messages['uc_recurring_renewal_expired_subject'] = t('[store-name]: [fee-title] expired');
  $messages['uc_recurring_renewal_expired_message'] = t("[order-first-name] [order-last-name], \n\n[fee-title] has expired, [order-link], at [store-name].\n\nYou can purchase a new order from [store-url].\n\nThanks again, \n\n[store-name]\n[site-slogan]");

  return $messages;
}

/*******************************************************************************
 * API functions
 ******************************************************************************/

/**
 * Process a renewal, either from the cron job or manually from a fee handler.
 *
 * @param $fee
 *   The fee object.
 * @return
 *   The new order ID or FALSE if unable to renew fee.
 */
function uc_recurring_renew($fee) {
univate's avatar
univate committed
  global $user;
  $order = uc_recurring_create_renewal_order($fee);

  if (uc_recurring_charge_profile($fee, $order) !== FALSE) {
    $order = uc_order_load($order->order_id);

    // Set new intervals.
    uc_recurring_set_intervals($fee);
    // Add the new order ID to database.
    $recurring_order = new stdClass();
    $recurring_order->original_order_id = $fee->order_id;
    $recurring_order->renewal_order_id = $order->order_id;
    drupal_write_record('uc_recurring_orders', $recurring_order);
    // Save the fee object.
    uc_recurring_fee_user_save($fee);

    module_invoke_all('recurring_renewal_completed', $order, $fee);
univate's avatar
univate committed
    // @todo - replace with rules
    //ca_pull_trigger('uc_recurring_renewal_complete', $order, $fee);

    // Return the new order ID.
    return $order->order_id;
  }
  else {
    // Charging failed.
    if (!$fee->own_handler) {
      uc_recurring_process_extensions($fee);
    }
univate's avatar
univate committed
    $message = t('Error: Recurring fee <a href="@orders-recurring-view">@fee</a> for product @model failed.', array('@orders-recurring-view' => url('admin/store/orders/recurring/view/fee/'. $fee->rfid), '@fee' => $fee->rfid, '@model' => $fee->data['model']));
    // Add comment to both new and original orders.
    uc_order_comment_save($fee->order_id, $user->uid, $message);
    uc_order_comment_save($order->order_id, $user->uid, $message);
    watchdog('uc_recurring', 'Failed to capture recurring fee of @amount for product @model on order @order_id.', array('@amount' => $fee->fee_amount, '@model' => $fee->data['model'], '@order_id' => $fee->order_id), WATCHDOG_ERROR, l(t('order !order_id', array('!order_id' => $fee->order_id)), 'admin/store/orders/'. $fee->order_id));

    module_invoke_all('recurring_renewal_failed', $order, $fee);
univate's avatar
univate committed
    // @todo - replace with rules
    //ca_pull_trigger('uc_recurring_renewal_failed', $order, $fee);
univate's avatar
univate committed
    uc_order_comment_save($fee->order_id, $user->uid, t('New recurring fee failed on order <a href="@store-orders">@order_id</a>.', array('@store-orders' => url('admin/store/orders/' . $order->order_id), '@order_id' => $order->order_id)));
  }
  return FALSE;
}

/**
 * Create a new order to be renewed via a recurring profile.
 */
function uc_recurring_create_renewal_order($fee) {
  global $user;
  $order = uc_order_load($fee->order_id);
  $old_id = $order->order_id;

  // Create a new order by cloning the current order and replacing order ID.
  $new_order = uc_order_new($order->uid, 'processing');
  // Add a comment in the new and original order history.
  uc_order_comment_save($old_id, $user->uid, t('New recurring fee processed, new order is <a href="@store-orders">@order_id</a>.', array('@store-orders' => url('admin/store/orders/'. $new_id), '@order_id' => $new_id)));
  uc_order_comment_save($new_id, $user->uid, t('Order created as a recurring fee for order <a href="@store-orders">@order_id</a>.', array('@store-orders' => url('admin/store/orders/'. $old_id), '@order_id' => $old_id)));

  $new_order = $order;
  $new_order->order_id = $new_id;
  // @todo we need a better way of tracking the relationship between orders.
  $new_order->data['recurring_fee'] = TRUE;
  $new_order->data['old_order_id'] = $old_id;
  $new_order->products = array();
univate's avatar
univate committed
  // We want the line items to be regenerated.
  unset($new_order->line_items);

  // Give other modules a chance to modify the new order before processing
  foreach (module_implements('recurring_renewal_pending') as $module) {
univate's avatar
univate committed
    $func = $module . '_recurring_renewal_pending';
univate's avatar
univate committed

  $new_order->line_items = uc_order_load_line_items($new_order, TRUE);

  uc_order_save($new_order);
  uc_order_update_status($new_id, 'pending');
  // We load the order to pick up the 'pending' state.
/**
 * Process a charge on a recurring profile.
 *
 * Invokes the renew callback, assumes renewal is successful unless FALSE is
 * returned.
 *
 * @param $fee
 *   The recurring fee object.
 * @param $order
 *   The ubercart order object.
 * @return
 *   TRUE if order charged.
 */
function uc_recurring_charge_profile(&$fee, &$order = NULL) {
  if (!isset($order)) {
    $order = uc_recurring_create_renewal_order($fee);
  return uc_recurring_invoke($fee->fee_handler, 'renew callback', array($order, &$fee));
/**
 * Process a fee expiration.
 * @param $fee
 *   The recurring fee object.
 */
function uc_recurring_expire($fee) {
  $order = uc_order_load($fee->order_id);

  $fee->status = UC_RECURRING_FEE_STATUS_EXPIRED;
  uc_recurring_fee_user_save($fee);

univate's avatar
univate committed
  // @todo - replace with rules
  //ca_pull_trigger('uc_recurring_renewal_expired', $order, $fee);
/**
 * Handle extensions when a recurring payment was unsuccessful.
 *
 * @param $fee
 *   The fee object.
 */
function uc_recurring_process_extensions(&$fee) {
  $extend_seconds = uc_recurring_get_extension($fee->pfid, $fee->attempts);
  uc_recurring_extend_fee($fee, $extend_seconds);
}
/**
 * Handle extensions when a recurring payment was unsuccessful.
 *
 * @param $fee
 *   The fee object.
 * @param $extend_seconds
 *   The number of seconds to extend the order by.
 */
function uc_recurring_extend_fee(&$fee, $extend_seconds) {
  $fee->attempts++;
  $fee->next_charge += $extend_seconds;
  $fee->data['extension'] += $extend_seconds;
  if ($extend_seconds <= 0) {
  uc_recurring_fee_user_save($fee);
}

/**
 * Returns the time to extend for a payment attempt.
 *
 * @param $fee_id
 *   The id of the recurring fee to get extensions.
 * @param $attempt
 *   The attempt number to return.
 */
function uc_recurring_get_extension($fee_id, $attempt) {
univate's avatar
univate committed
  $extension = db_query("SELECT * FROM {uc_recurring_extensions} WHERE (pfid = :pfid OR pfid IS NULL) AND rebill_attempt = :rebill_attempt ORDER BY pfid DESC", array(':pfid' => $fee_id, ':rebill_attempt' => $attempt))->fetchObject();
univate's avatar
univate committed
  if ($extension != FALSE) {
    $extend_seconds = $extension->time_to_extend;
  }
  return $extend_seconds;
 * Retuns a list of all the extensions for a specific recurring fee.
 *
 * @param $fee_id
 *   The id of the recurring fee to get extensions.
function uc_recurring_get_extension_list($fee_id = NULL) {
  if ($fee_id === NULL) {
univate's avatar
univate committed
    // TODO Please convert this statement to the D7 database API syntax.
    $result = db_query("SELECT * FROM {uc_recurring_extensions} WHERE pfid IS NULL ORDER BY pfid DESC, rebill_attempt ASC");
  }
  else {
univate's avatar
univate committed
    $result = db_query("SELECT * FROM {uc_recurring_extensions} WHERE (pfid = :pfid OR pfid IS NULL) ORDER BY pfid DESC, rebill_attempt ASC", array(':pfid' => $fee_id));
univate's avatar
univate committed
  foreach ($result as $extension) {
    if (!isset($extensions[$extension->rebill_attempt])) {
      $extensions[$extension->rebill_attempt] = $extension;
    }
  }
  return $extensions;
}

/**
 * Save a set of extensions.
 *
 * @param $extensions
 *   String of comma seperated day values to extend the extension.
 * @param $extend_seconds
 *   The number of seconds to extend the order by.
 */
function uc_recurring_save_extensions($extensions, $fee_id = NULL) {
univate's avatar
univate committed
  // TODO Please review the conversion of this statement to the D7 database API syntax.
  /* db_query("DELETE FROM {uc_recurring_extensions} WHERE pfid IS NULL") */
  db_delete('uc_recurring_extensions')
  ->condition('pfid', NULL)
  ->execute();
  foreach ($extend as $days_to_extend) {
univate's avatar
univate committed
    $seconds = $days_to_extend * (24 * 60 * 60);
    // TODO Please review the conversion of this statement to the D7 database API syntax.
    /* db_query("INSERT INTO {uc_recurring_extensions} (rebill_attempt, time_to_extend) VALUES (%d, %d)", $count, $seconds) */
    $id = db_insert('uc_recurring_extensions')
      ->fields(array(
        'rebill_attempt' => $count,
        'time_to_extend' => $seconds,
      ))
      ->execute();
    $count++;
  }
  // Last extension set extension to 0 to expire.
univate's avatar
univate committed
  // TODO Please review the conversion of this statement to the D7 database API syntax.
  /* db_query("INSERT INTO {uc_recurring_extensions} (rebill_attempt, time_to_extend) VALUES (%d, 0)", $count) */
  $id = db_insert('uc_recurring_extensions')
    ->fields(array(
      'rebill_attempt' => $count,
      'time_to_extend' => 0,
    ))
    ->execute();
/**
 * Get the recurring handlers info.
 *
 * @return
 *   Array keyed by the implementing module, and the callback.
 */
function uc_recurring_get_recurring_info($key = '', $reset = FALSE) {
  static $data = array();

  if ($reset || ($key && empty($data[$key])) || (!$key && empty($data))) {
    $data = array();
    foreach (module_implements('recurring_info') as $module) {
      if ($result = module_invoke($module, 'recurring_info')) {
        $data[] = $result;
      }
    }
    // Get uc recurring own implementation. The pattern of the function is
    // uc_recurring_MODULE-NAME_recurring_info().
    if ($modules = uc_recurring_includes()) {
      foreach ($modules as $module) {
univate's avatar
univate committed
        $func = 'uc_recurring_' . $module . '_recurring_info';
        if (function_exists($func) && $result = call_user_func($func)) {
          $data[] = $result;
        }
      }
    }
    // Normalize data array to be keyed by the fee handler name.
    $data = _uc_recurring_get_recurring_info_handlers($data);
univate's avatar
univate committed
    drupal_alter('recurring_info', $data);
    $return = !empty($data[$key]) ? $data[$key] : array();
/**
 * Default a recurring default handler, does nothing except returning TRUE.
 */
function uc_recurring_default_handler() {
  return TRUE;
}

/**
 * Checks that a payment method can process recurring fees. This is done by
 * checking that a recurring fee handler exists for the payment method.
 * @param $payment method
 *   The id of the payment method.
 * @return
 *   TRUE is a valid fee handler for this payment method is found.
 */
function uc_recurring_payment_method_supported($payment_method) {
  $info = uc_recurring_get_recurring_info();
  if (isset($info[$payment_method]['fee handler'])) {
    return !empty($info[$info[$payment_method]['fee handler']]);
  }
  return FALSE;
 * Include uc recurring own payment gateway implementations.
 *
 * @return
 *   The modules that were included.
 */
function uc_recurring_includes() {
  $return = array();
  $modules = array('uc_authorizenet', 'test_gateway', 'uc_credit', 'uc_payment_pack');
  foreach ($modules as $module) {
    if (module_exists($module)) {
univate's avatar
univate committed
      module_load_include('inc', 'uc_recurring', '/includes/uc_recurring.' . $module);
      $return[] = $module;
    }
  }
  return $return;
}

/**
 * Invoke an item type specific function, which will be item types
 * base appended with _$op. The parameters given in $params will be
 * passed to this function.
 *
 * @param $handler
 *   The handler from the fee object.
 * @param $op
 *   The operation that should be invoked.
 * @param $params
 *   The paramaters that should be passed to the handler.
 */
function uc_recurring_invoke($handler, $op, $params = array()) {
  $info = uc_recurring_get_recurring_info($handler);
  $function = $info[$op];
  if (function_exists($function)) {
    // Add the op argument to the params, in case the handler will need to use
    // it to identify the operation it is in.
    $params[] = $op;
    return call_user_func_array($function, $params);
  }
}

/**
 * Saves a recurring fee for a user.
 *
 * @param $fee
 *   A fee object.
 * @return
 *   The reccuring fee ID of the saved fee.
 */
function uc_recurring_fee_user_save($fee) {
  // Update an existing row.
  drupal_alter('recurring_fee_user_save', $fee);

  if (!empty($fee->rfid)) {
    // Update an existing row.
    drupal_write_record('uc_recurring_users', $fee, array('rfid'));
  }
  else {
    drupal_write_record('uc_recurring_users', $fee);
  }
  return $fee->rfid;
}

/**
 * Loads a recurring fee from a user.
 *
 * @param $rfid
 *   The recurring fee ID to load.
 * @return
 *   The recurring fee object.
 */
function uc_recurring_fee_user_load($rfid) {
  $fee = db_query("SELECT ru.*, u.name FROM {uc_recurring_users} ru JOIN users u ON u.uid = ru.uid WHERE rfid = :rfid", array(':rfid' => $rfid))->fetchObject();
  if ($fee) {
    $fee->data = unserialize($fee->data);
    // Allow other modules to change the saved data.
    drupal_alter('recurring_fee_user_load', $product);
}

/**
 * Delete a recurring fee from a user.
 *
 * @param $rfid
 *   The ID of the recurring fee to be removed from the appropriate table.
 */
function uc_recurring_fee_user_delete($rfid) {
  module_invoke_all('recurring_user_delete', $rfid);
univate's avatar
univate committed
  // TODO Please review the conversion of this statement to the D7 database API syntax.
  /* db_query("DELETE FROM {uc_recurring_users} WHERE rfid = %d", $rfid) */
  db_delete('uc_recurring_users')
  ->condition('rfid', $rfid)
  ->execute();
}

/**
 * Wrapper function to cancel a user's recurring fee.
 *
 * Cancellation is done by setting remaining intervals to 0.
 *
 * @param $rfid
 *   The recurring fee's ID.
 * @param $fee
 *   Optional; The loaded fee object.
 */
function uc_recurring_fee_cancel($rfid, $fee = NULL) {
  global $user;
  if (empty($fee)) {
    $fee = uc_recurring_fee_user_load($rfid);
  }
  $remaining = $fee->remaining_intervals;
  $fee->remaining_intervals = 0;
  // Add a timestamp to the user cancellation.
univate's avatar
univate committed
  $fee->data['cancel'] = REQUEST_TIME;
  uc_recurring_invoke($fee->fee_handler, 'cancel callback', array($fee));

  // Add comment about cancellation in the product.
univate's avatar
univate committed
  uc_order_comment_save($fee->order_id, $user->uid, t('<a href="@user-link">@user</a> cancelled recurring fee <a href="@fee-link">@fee</a>. There were @remaining fee(s) still pending.', array('@user-link' => url('user/' . $user->uid), '@user' => $user->name, '@fee-link' => url('admin/store/orders/recurring/view/fee/' . $rfid), '@fee' => $rfid, '@remaining' => $remaining < 0 ? 'unlimited' : $remaining)));

  // Let other modules act on the canceled fee.
  module_invoke_all('uc_recurring_cancel', $fee);

  $order = uc_order_load($fee->order_id);
univate's avatar
univate committed
  // @todo - replace with rules
  //ca_pull_trigger('uc_recurring_cancel', $order, $fee);
}

/**
 * Get an array of recurring fees associated with any product on an order.
 *
 * @param $order
 *   The order object in question.
 * @param $reset
 *   TRUE if the fees cache should be reset.
 * @return
 *   An array of recurring fee objects containing all their data from the DB.
 */
function uc_recurring_get_fees($order, $reset = FALSE) {
  static $fees = array();
  if ($reset || empty($fees[$order->order_id])) {
    if (!empty($order->products)) {
      $products = array();
      foreach ($order->products as $value) {
        $products[$value->order_product_id] = $value->order_product_id;
      }

      $products[] = 1;
      // TODO Please review the conversion of this statement to the D7 database API syntax.
univate's avatar
univate committed
      $result = db_query("SELECT ru.*, u.name FROM {uc_recurring_users} ru LEFT JOIN {users} u ON u.uid=ru.uid WHERE order_product_id IN (:products)", array(':products' => $products));

      if (!empty($result)) {
        foreach ($result as $fee) {
univate's avatar
univate committed
          $fee->data = unserialize($fee->data);
          $fees[$order->order_id][] = $fee;
        }
      }
    }
  }
  return !empty($fees[$order->order_id]) ? $fees[$order->order_id] : array();
}

/**
 * Get all pending fees that should be renewed.
 */
function uc_recurring_get_fees_for_renew() {
univate's avatar
univate committed
  $fees = array();
  $result = db_query("SELECT * FROM {uc_recurring_users} WHERE remaining_intervals <> :remaining_intervals AND next_charge <= :next_charge AND own_handler = :own_handler ORDER BY order_id DESC", array(':remaining_intervals' => 0, ':next_charge' => REQUEST_TIME, ':own_handler' => 0));
  foreach ($result as $fee) {
    $fee->data = unserialize($fee->data);
    $fees[$fee->rfid] = $fee;
  }
  return $fees;
}

/**
 * Get all pending fees that should be expired.
 */
function uc_recurring_get_fees_for_expiration() {
univate's avatar
univate committed
  $fees = array();
  $result = db_query("SELECT * FROM {uc_recurring_users} WHERE remaining_intervals = :remaining_intervals AND next_charge <= :next_charge AND own_handler = :own_handler AND status <> :status ORDER BY order_id DESC", array(':remaining_intervals' => 0, ':next_charge' => REQUEST_TIME, ':own_handler' => 0, ':status' => UC_RECURRING_FEE_STATUS_EXPIRED));
  foreach ($result as $fee) {
    $fee->data = unserialize($fee->data);
    $fees[$fee->rfid] = $fee;
  }
  return $fees;
}


function uc_recurring_get_all_fees($pager = FALSE, $order = '') {
univate's avatar
univate committed
  $fees = array();
  //$sql = "SELECT ru.*, u.name FROM {uc_recurring_users} ru LEFT JOIN {users} u ON u.uid=ru.uid" . $order;
  $query = db_select('uc_recurring_users', 'ru');
  if ($pager) {
    $query->extend('PagerDefault');

  // add the order header
  $query->extend('TableSort')
    ->orderByHeader($order)
    ->fields('ru')
    ->fields('u', array('name'))
    ->join('users', 'u', 'u.uid = ru.uid');

  $result = $query->execute();

univate's avatar
univate committed
  foreach ($result as $fee) {
    $fee->data = unserialize($fee->data);
  }
  return $fees;
}

/**
 * Get an array of recurring fees associated with a user.
 *
 * @param $order
 *   The order object in question.
 * @param $reset
 *   TRUE if the fees cache should be reset.
 * @return
 *   An array of recurring fee objects containing all their data from the DB.
 */
function uc_recurring_get_user_fees($uid) {
univate's avatar
univate committed
  $fees = array();
  $result = db_query("SELECT * FROM {uc_recurring_users} WHERE uid = :uid AND status <> :status ORDER BY order_id DESC", array(':uid' => $uid, ':status' => UC_RECURRING_FEE_STATUS_EXPIRED));
  foreach ($result as $fee) {
/*******************************************************************************
 * Callback Functions
 ******************************************************************************/

/**
 * Restrict access to recurring fee operations for users.
 *
 * @param $account
 *   The user account being accessed
 * @param $rfid
 *   The recurring fee ID.
 * @param $op
 *   The user operation, e.g. cancel, edit, update.
 * @return
 *   True if user has permission to access menu item.
 */
function uc_recurring_user_access($account = NULL, $rfid = NULL, $op = '') {
univate's avatar
univate committed
    return user_access('administer recurring fees');
  }
  // Check if user has access to perform action on a certain fee.
  if (isset($rfid)) {
    $fee = uc_recurring_fee_user_load($rfid);
    $access = module_invoke_all('recurring_access', $fee, $op, $account);
    if (in_array(UC_RECURRING_ACCESS_DENY, $access, TRUE)) {
      return FALSE;
    }
    elseif (in_array(UC_RECURRING_ACCESS_ALLOW, $access, TRUE)) {
      return TRUE;
    }
  }
  return (user_access('administer recurring fees') || (user_access('view own recurring fees') && $user->uid == $account->uid)) && uc_recurring_get_user_fees($account->uid);
}

/*******************************************************************************
 * Theme Functions
 ******************************************************************************/

 * Displays a table for users to administer their recurring fees.
 *
 * TODO: This theme function should receive as an argument the fees to be
 * included in the table, not simply a user ID. All the logic to load fees
 * should be taken care of in the function that calls this theme function.
univate's avatar
univate committed
function theme_uc_recurring_user_table($variables) {
  $uid = $variables['uid'];
  drupal_add_css(drupal_get_path('module', 'uc_recurring') . '/uc_recurring.css');

  // Set up a header array for the table.
  $header = array(t('Order'), t('Amount'), t('Interval'), t('Next charge'), t('Status'), t('Remaining'), t('Options'));
  $recurring_states = uc_recurring_fee_status_label();

  // Build an array of rows representing the user's fees.
  $rows = array();

  foreach (uc_recurring_get_user_fees($uid) as $fee) {
    // Get the user operations links for the current fee.
    $ops = uc_recurring_get_fee_ops('user', $fee);

    // Add the row to the table for display.
    $rows[] = array(
univate's avatar
univate committed
        l($fee->order_id, 'user/' . $uid . '/order/' . $fee->order_id),
        theme('uc_price', $fee->fee_amount),
        array(
          'data' => check_plain($fee->regular_interval),
          'nowrap' => 'nowrap',
        ),
        format_date($fee->next_charge, 'short'),
        '<span class="recurring-status-' . intval($fee->status) . '">' . $recurring_states[$fee->status] . '</span>',
        $fee->remaining_intervals < 0 ? t('Until cancelled') : $fee->remaining_intervals,
univate's avatar
univate committed
        array(
          'data' => implode(' | ', $ops),
          'nowrap' => 'nowrap',
        ),
univate's avatar
univate committed
    $rows[] = array(array(
        'data' => t('Your account has no recurring fees.'),
        'colspan' => 7,
      ));
univate's avatar
univate committed
  return theme('table', array('header' => $header, 'rows' => $rows));
/**
 * Displays a table for users to administer their recurring fees.
 *
 * TODO: This theme function should receive as an argument the fees to be
 * included in the table, not simply a user ID. All the logic to load fees
 * should be taken care of in the function that calls this theme function.
 */
univate's avatar
univate committed
function theme_uc_recurring_admin_table($variables) {
  $fees = $variables['fees'];
  // Build the table header.
  $header = array(
univate's avatar
univate committed
    array(