You are here

uc_recurring.module in UC Recurring Payments and Subscriptions 6.2

Same filename and directory in other branches
  1. 6 uc_recurring.module
  2. 7.2 uc_recurring.module

Allows you to add a recurring fee to a product/SKU to handle subscription type services.

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

File

uc_recurring.module
View source
<?php

/**
 * @file
 * Allows you to add a recurring fee to a product/SKU to handle subscription
 * type services.
 *
 * 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);
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
 ******************************************************************************/

/**
 * Implementation of hook_init().
 */
function uc_recurring_init() {
  module_load_include('inc', 'uc_recurring', 'uc_recurring.ca');
}

/**
 * Implementation of 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',
    'page arguments' => array(
      5,
      6,
    ),
    'access arguments' => array(
      'administer recurring fees',
    ),
    'type' => MENU_CALLBACK,
    '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',
    'page arguments' => array(
      5,
      6,
    ),
    'access arguments' => array(
      'administer recurring fees',
    ),
    'type' => MENU_CALLBACK,
    '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']),
          );
          $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.
          $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.
        }
      }
    }
  }
  return $items;
}

/**
 * Implementation of hook_perm().
 */
function uc_recurring_perm() {
  return array(
    'administer recurring fees',
    'view own recurring fees',
  );
}

/**
 * Implementation of hook_theme().
 */
function uc_recurring_theme() {
  return array(
    'uc_recurring_user_table' => array(
      'arguments' => array(
        'uid' => NULL,
      ),
    ),
    'uc_recurring_admin_table' => array(
      'arguments' => array(
        'fees' => NULL,
      ),
    ),
  );
}

/**
 * Implementation of hook_recurring_info().
 *
 * Add a default handler.
 */
function uc_recurring_recurring_info() {
  $items['default'] = array(
    'name' => t('Default handler'),
    'module' => 'uc_recurring',
    'fee handler' => 'default',
    'process callback' => 'uc_recurring_default_handler',
    'renew callback' => 'uc_recurring_default_handler',
    'menu' => array(
      'charge' => array(
        'title' => 'Charge',
        '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'),
      ),
      'edit' => array(
        'title' => 'Edit',
        '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'),
      ),
      'cancel' => array(
        'title' => 'Cancel',
        'page arguments' => array(
          'uc_recurring_user_cancel_form',
        ),
        'file' => 'uc_recurring.pages.inc',
        'file path' => drupal_get_path('module', 'uc_recurring'),
      ),
    ),
  );
  return $items;
}

/**
 * Implementation of hook_cron().
 *
 * On the renewal time of a recurring fee see if the payment method would
 * like to perform any addition actions.
 * 
 * LOCK AGAINST DUPLICATE CALLS OF THIS FUNCTION:
 * ----------------------------------------------
 * A lock is set for a default 30sec - this can be overridden by altering the
 * uc_recurring_cron_timeout variable
 */
function uc_recurring_cron() {
  if (!lock_acquire('uc_recurring_cron', variable_get('uc_recurring_cron_timeout', 30))) {

    // Don't run the recurring payments while another process has this lock.
    return FALSE;
  }
  if (variable_get('uc_recurring_trigger_renewals', TRUE)) {
    $fees = uc_recurring_get_fees_for_renew();
    if (!empty($fees)) {
      $fail = $success = 0;
      foreach ($fees as $fee) {
        if ($order_id = uc_recurring_renew($fee)) {
          $success++;
        }
        else {

          // payment attempted but failed
          $fail++;
        }
        if (lock_may_be_available('uc_recurring_cron')) {

          // exit if the lock has expired
          break;
        }
      }
      watchdog('uc_recurring', '@success recurring fees processed successfully; @fail failed.', array(
        '@success' => $success,
        '@fail' => $fail,
      ));
    }
  }
  lock_release('uc_recurring_cron');
  $fees = uc_recurring_get_fees_for_expiration();
  if (!empty($fees)) {
    foreach ($fees as $fee) {
      uc_recurring_expire($fee);
    }
    watchdog('uc_recurring', '@count recurring fees expired successfully.', array(
      '@count' => count($fees),
    ));
  }
}

/**
 * Implementation of hook_user().
 */
function uc_recurring_user($op, &$edit, &$account, $category = NULL) {
  switch ($op) {
    case 'view':

      // Show only if user is privileged, and there are recurring fees.
      if (uc_recurring_user_access($account)) {
        $account->content['recurring_fee'] = array(
          '#type' => 'user_profile_category',
          '#weight' => -3,
          '#title' => t('Recurring fees'),
          'table' => array(
            '#type' => 'user_profile_item',
            '#value' => l('Click here to view your recurring fees', 'user/' . $account->uid . '/recurring-fees'),
          ),
        );
      }
      break;
  }
}

/**
 * Implementation of hook_token_values(). (token.module)
 */
function uc_recurring_token_values($type, $object = NULL) {
  $values = array();
  switch ($type) {
    case 'recurring_fee':
      $fee = $object;
      $values['recurring-fee-id'] = $fee->rfid;
      $values['next-charge'] = format_date($fee->next_charge);
      $values['fee-amount'] = uc_store_format_price_field_value($fee->fee_amount);
      $values['fee-title'] = $fee->fee_title;
      $values['charged-intervals'] = $fee->charged_intervals;
      $values['remaining-intervals'] = $fee->remaining_intervals < 0 ? t('Until cancelled') : $fee->remaining_intervals;
      $values['renewal-attempts'] = $fee->attempts;
      $values['recurring-link'] = url('user/' . $fee->uid . '/recurring-fees', array(
        'absolute' => TRUE,
      ));
      break;
  }
  return $values;
}

/**
 * Implementation of hook_token_list(). (token.module)
 */
function uc_recurring_token_list($type = 'all') {
  $tokens = array();
  if ($type == 'recurring_fee' || $type == 'ubercart' || $type == 'all') {
    $tokens['recurring_fee']['recurring-fee-id'] = t('The recurring fee ID.');
    $tokens['recurring_fee']['next-charge'] = t('The date and time when the next charge is due to be processed.');
    $tokens['recurring_fee']['fee-amount'] = t('The recurring fee due on the next charge.');
    $tokens['recurring_fee']['fee-title'] = t('The product title used as orders for this recurring fee.');
    $tokens['recurring_fee']['charged-intervals'] = t('The number of recurring intervals due left in subscription.');
    $tokens['recurring_fee']['remaining-intervals'] = t('The number of recurring intervals due left in subscription.');
    $tokens['recurring_fee']['renewal-attempts'] = t('The number of attempts to try and renew this fee. ');
  }
  return $tokens;
}

/**
 * Display users recurring fees.
 */
function uc_recurring_user_fees($user) {
  return theme('uc_recurring_user_table', $user->uid);
}

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

/**
 * Implementation of hook_order().
 */
function uc_recurring_order($op, $arg1, $arg2) {
  switch ($op) {
    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);
      }
      break;
  }
}

/**
 * Implementation of 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);
      $result = db_result(db_query("SELECT COUNT(*) FROM {uc_payment_receipts} WHERE order_id = '%d'", $order->order_id));
      if ($result) {
        db_query("UPDATE {uc_payment_receipts} SET uid = '%d' WHERE order_id='%d'", $order->uid, $order->order_id);
      }
    }
  }
}

/**
 * Implementation of 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) {
  if ($fee->attempts == 0) {

    // create a new order for the renewal
    $order = uc_recurring_create_renewal_order($fee);
  }
  else {

    // renewal order already created on first charge attempt
    $order = FALSE;
    if (isset($fee->data['retry_order_id']) && is_numeric($fee->data['retry_order_id'])) {
      $order = uc_order_load($fee->data['retry_order_id']);
    }
    if ($order == FALSE) {

      // could not find the last renewal order so create a new one
      $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);
    if (isset($fee->data['retry_order_id'])) {
      unset($fee->data['retry_order_id']);
    }

    // Save the fee object.
    uc_recurring_fee_user_save($fee);
    uc_recurring_renewal_module_invoke('recurring_renewal_completed', $order, $fee);
    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);
    }
    if ($fee->module == 'uc_recurring_product') {
      $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'],
      ));
    }
    else {
      $message = t('Error: Recurring fee <a href="@orders-recurring-view">@fee</a> for order @order_id failed.', array(
        '@orders-recurring-view' => url('admin/store/orders/recurring/view/fee/' . $fee->rfid),
        '@fee' => $fee->rfid,
        '@order_id' => $fee->order_id,
      ));
    }

    // 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);
    if ($fee->module == 'uc_recurring_product') {
      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));
    }
    else {
      watchdog('uc_recurring', 'Failed to capture recurring fee of @amount for order @order_id.', array(
        '@amount' => $fee->fee_amount,
        '@order_id' => $fee->order_id,
      ), WATCHDOG_ERROR, l(t('order !order_id', array(
        '!order_id' => $fee->order_id,
      )), 'admin/store/orders/' . $fee->order_id));
    }

    // check if there will be a retry
    $extension = uc_recurring_get_extension($fee->pfid, $fee->attempts);
    if ($extension) {
      $fee->data['retry_order_id'] = $order->order_id;
      uc_recurring_fee_user_save($fee);
    }
    uc_recurring_renewal_module_invoke('recurring_renewal_failed', $order, $fee);
    ca_pull_trigger('uc_recurring_renewal_failed', $order, $fee);
    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;

  // Clear CC cache so that correct info is loaded (if any)
  if (module_exists('uc_credit')) {
    uc_credit_cache('clear');
  }
  $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, 'post_checkout');
  $new_id = $new_order->order_id;

  // 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;
  $new_order->created = time();
  $new_order->order_status = 'processing';

  // @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();

  // 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
  uc_recurring_renewal_module_invoke('recurring_renewal_pending', $new_order, $fee);
  $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.
  $new_order = uc_order_load($new_id);
  return $new_order;
}

/**
 * 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);
  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) {
    $fee->remaining_intervals = 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) {
  $result = db_query("SELECT * FROM {uc_recurring_extensions} WHERE (pfid = %d OR pfid IS NULL) AND rebill_attempt = %d ORDER BY pfid DESC", $fee_id, $attempt);
  $extend_seconds = 0;
  if ($result != FALSE) {
    $extension = db_fetch_object($result);
    $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) {
    $result = db_query("SELECT * FROM {uc_recurring_extensions} WHERE pfid IS NULL ORDER BY pfid DESC, rebill_attempt ASC");
  }
  else {
    $result = db_query("SELECT * FROM {uc_recurring_extensions} WHERE (pfid = %d OR pfid IS NULL) ORDER BY pfid DESC, rebill_attempt ASC", $fee_id);
  }
  $extensions = array();
  while ($extension = db_fetch_object($result)) {
    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) {
  db_query("DELETE FROM {uc_recurring_extensions} WHERE pfid IS NULL");
  $extend = explode(',', $extensions);
  $count = 0;
  foreach ($extend as $days_to_extend) {
    $seconds = $days_to_extend * (24 * 60 * 60);
    db_query("INSERT INTO {uc_recurring_extensions} (rebill_attempt, time_to_extend) VALUES (%d, %d)", $count, $seconds);
    $count++;
  }

  // Last extension set extension to 0 to expire.
  db_query("INSERT INTO {uc_recurring_extensions} (rebill_attempt, time_to_extend) VALUES (%d, 0)", $count);
}

/**
 * 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) {
        $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);
    drupal_alter('recurring_info', $data);
  }
  if (!empty($key)) {
    $return = !empty($data[$key]) ? $data[$key] : array();
  }
  else {
    $return = $data;
  }
  return $return;
}

/**
 * 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',
    'uc_cybersource',
    'test_gateway',
    'uc_credit',
    'uc_payment_pack',
  );
  foreach ($modules as $module) {
    if (module_exists($module)) {
      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);

  // Check if there's a custom function for this $op.
  if (!empty($info[$op]) && function_exists($info[$op])) {

    // 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($info[$op], $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);
  }

  // Allow modules to do things with the full object (always with rfid).
  module_invoke_all('recurring_fee_user_saved', $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_fetch_object(db_query("SELECT * FROM {uc_recurring_users} WHERE rfid = %d", $rfid));
  if ($fee) {
    $fee->data = unserialize($fee->data);

    // Allow other modules to change the saved data.
    drupal_alter('recurring_fee_user_load', $fee);
    return $fee;
  }
  return FALSE;
}

/**
 * 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);
  db_query("DELETE FROM {uc_recurring_users} WHERE rfid = %d", $rfid);
}

/**
 * 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;
  $fee->status = UC_RECURRING_FEE_STATUS_CANCELLED;

  // Add a timestamp to the user cancellation.
  $fee->data['cancel'] = time();
  uc_recurring_fee_user_save($fee);
  uc_recurring_invoke($fee->fee_handler, 'cancel callback', array(
    $fee,
  ));

  // Add comment about cancellation in the product.
  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);
  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;
      }
      $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 (" . db_placeholders($products) . ")", $products);
      while ($fee = db_fetch_object($result)) {
        $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() {
  $fees = array();
  $result = db_query("SELECT * FROM {uc_recurring_users} WHERE remaining_intervals <> 0 AND next_charge <= %d AND status = %d AND own_handler = 0 ORDER BY order_id DESC", time(), UC_RECURRING_FEE_STATUS_ACTIVE);
  while ($fee = db_fetch_object($result)) {
    $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() {
  $fees = array();
  $result = db_query("SELECT * FROM {uc_recurring_users} WHERE remaining_intervals = 0 AND next_charge <= %d AND own_handler = 0 AND status <> %d ORDER BY order_id DESC", time(), UC_RECURRING_FEE_STATUS_EXPIRED);
  while ($fee = db_fetch_object($result)) {
    $fee->data = unserialize($fee->data);
    $fees[$fee->rfid] = $fee;
  }
  return $fees;
}

/**
 * Get all fees is the system.
 */
function uc_recurring_get_all_fees($pager = FALSE, $order = '') {
  $fees = array();
  $sql = "SELECT ru.*, u.name FROM {uc_recurring_users} ru LEFT JOIN {users} u ON u.uid=ru.uid" . $order;
  if ($pager) {
    $result = pager_query($sql, variable_get('uc_order_number_displayed', 30));
  }
  else {
    $result = db_query($sql);
  }
  while ($fee = db_fetch_object($result)) {
    $fees[$fee->rfid] = $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) {
  $fees = array();
  $result = db_query("SELECT * FROM {uc_recurring_users} WHERE uid = %d AND status <> %d ORDER BY order_id DESC", $uid, UC_RECURRING_FEE_STATUS_EXPIRED);
  while ($fee = db_fetch_object($result)) {
    $fees[$fee->rfid] = $fee;
  }
  return $fees;
}

/*******************************************************************************
 * 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 = '') {
  global $user;
  if (!isset($account)) {
    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;
    }
    $fees = uc_recurring_get_user_fees($account->uid);
    if (user_access('view own recurring fees') && $user->uid == $account->uid && $fees) {

      // Make sure that the currently logged-in user actually owns the recurring
      // fee they are attempting to access or has administrative privileges.
      if ($fee->uid != $user->uid) {
        return user_access('administer recurring fees');
      }
    }
  }
  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.
 */
function theme_uc_recurring_user_table($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'),
  );
  $context = array(
    'revision' => 'themed-original',
    'location' => 'recurring-user-table',
  );
  $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(
      'data' => array(
        l($fee->order_id, 'user/' . $uid . '/order/' . $fee->order_id),
        uc_price($fee->fee_amount, $context),
        array(
          'data' => check_plain($fee->regular_interval),
          'nowrap' => 'nowrap',
        ),
        format_date($fee->next_charge, 'small'),
        '<span class="recurring-status-' . intval($fee->status) . '">' . $recurring_states[$fee->status] . '</span>',
        $fee->remaining_intervals < 0 ? t('Until cancelled') : $fee->remaining_intervals,
        array(
          'data' => implode(' | ', $ops),
          'nowrap' => 'nowrap',
        ),
      ),
    );
  }
  if (empty($rows)) {
    $rows[] = array(
      array(
        'data' => t('Your account has no recurring fees.'),
        'colspan' => 7,
      ),
    );
  }
  return theme('table', $header, $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.
 */
function theme_uc_recurring_admin_table($fees) {

  // Build the table header.
  $header = array(
    array(
      'data' => t('ID'),
      'field' => 'ru.rfid',
    ),
    array(
      'data' => t('Order'),
      'field' => 'ru.order_id',
    ),
    array(
      'data' => t('Status'),
      'field' => 'ru.status',
    ),
    array(
      'data' => t('Account'),
    ),
    array(
      'data' => t('Next'),
      'field' => 'ru.next_charge',
      'sort' => 'asc',
    ),
    array(
      'data' => t('Amount'),
    ),
    array(
      'data' => t('Interval'),
    ),
    array(
      'data' => t('Left'),
    ),
    array(
      'data' => t('Total'),
    ),
    array(
      'data' => t('Operations'),
    ),
  );
  $context = array(
    'revision' => 'themed-original',
    'location' => 'recurring-admin',
  );
  $recurring_states = uc_recurring_fee_status_label();
  foreach ($fees as $fee) {
    if ($fee->remaining_intervals < 0) {
      $fee_remaining = t('Until cancelled');
    }
    elseif ($fee->remaining_intervals == 0) {
      $fee_remaining = '-';
    }
    else {
      $fee_remaining = $fee->remaining_intervals;
    }

    // Get the administrator operations links for the current fee.
    $ops = uc_recurring_get_fee_ops('fee_admin', $fee);

    // Add a row for the current fee to the table.
    $rows[] = array(
      l($fee->rfid, 'admin/store/orders/recurring/view/fee/' . $fee->rfid),
      l($fee->order_id, 'admin/store/orders/' . $fee->order_id),
      $recurring_states[$fee->status],
      l($fee->name, 'user/' . $fee->uid),
      format_date($fee->next_charge, 'small'),
      $fee->fee_amount != '0.00' ? uc_price($fee->fee_amount, $context) : t('Same as product price'),
      array(
        'data' => check_plain($fee->regular_interval),
        'nowrap' => 'nowrap',
      ),
      $fee_remaining,
      $fee->remaining_intervals < 0 ? $fee->charged_intervals : $fee->remaining_intervals + $fee->charged_intervals,
      array(
        'data' => implode(' | ', $ops),
        'nowrap' => 'nowrap',
      ),
    );
  }

  // Add the table and pager to the page output.
  $output .= theme('table', $header, $rows);
  $output .= theme('pager', NULL, variable_get('uc_order_number_displayed', 30), 0);
  return $output;
}

/*******************************************************************************
 * Helper Functions
 ******************************************************************************/

/**
 * Returns the human readable labels for defined statuses.
 *
 * @param $status
 *   Optionally specify a status and return only its label.
 *
 * @return
 *   An array of recurring fee status labels or a single label if specified.
 */
function uc_recurring_fee_status_label($status = NULL) {

  // Define the array for defined statuses.
  $status_labels = array(
    UC_RECURRING_FEE_STATUS_ACTIVE => t('Active'),
    UC_RECURRING_FEE_STATUS_EXPIRED => t('Expired'),
    UC_RECURRING_FEE_STATUS_CANCELLED => t('Cancelled'),
    UC_RECURRING_FEE_STATUS_SUSPENDED => t('Suspended'),
  );
  drupal_alter('uc_recurring_status', $status_labels);

  // Return the specific status if specified.
  if (!is_null($status)) {
    return isset($status_labels[$status]) ? $status_labels[$status] : FALSE;
  }
  return $status_labels;
}

/**
 * Get the data of the handlers, keyed by the fee handler.
 *
 * @param $result
 *   The raw result returned from invoking hook_uc_recurring_info().
 *
 * @see uc_recurring_get_recurring_info()
 */
function _uc_recurring_get_recurring_info_handlers($data = array()) {
  $return = array();
  foreach ($data as $module) {
    foreach ($module as $name => $handler) {
      $return[$name] = $handler;
    }
  }
  return $return;
}

/**
 * Provide default options.
 */
function uc_recurring_get_fee_ops($context, $fee) {
  $ops = array();
  $info = uc_recurring_get_recurring_info($fee->fee_handler);
  if (!empty($info['menu'])) {
    foreach ($info['menu'] as $path => $menu) {
      if ($menu != UC_RECURRING_MENU_DISABLED) {
        if ($context == 'fee_admin') {
          $full_path = 'admin/store/orders/recurring/' . $fee->rfid . '/' . $path . '/' . $info['fee handler'];
        }
        else {
          $full_path = 'user/' . $fee->uid . '/recurring/' . $fee->rfid . '/' . $path . '/' . $info['fee handler'];
        }
        if (menu_valid_path(array(
          'link_path' => $full_path,
        ))) {
          $ops[$path] = l($path, $full_path, array(
            'query' => drupal_get_destination(),
          ));
        }
      }
    }
  }
  return $ops;
}

/**
 * Set the intervals after a successful charge.
 * @param $fee
 *   The fee object passed by reference.
 */
function uc_recurring_set_intervals(&$fee) {
  $fee->next_charge = strtotime('+' . $fee->regular_interval, $fee->next_charge) - $fee->data['extension'];
  if ($fee->remaining_intervals > 0) {
    $fee->remaining_intervals--;
  }
  else {
    $order = uc_order_load($fee->order_id);
  }
  $fee->charged_intervals++;
  $fee->attempts = 0;
  $fee->data['extension'] = 0;
}

/**
 * Invokes uc_recurring renewal hooks in every module.
 *
 * We cannot use module_invoke() for this, because the arguments need to
 * be passed by reference.
 * @param $type
 *   The renewal hook type. Available type recurring_renewal_pending,
 *     recurring_renewal_completed, recurring_renewal_failed.
 * @param $order
 *   The order object.
 * @param $fee
 *   The recurring Fee object.
 */
function uc_recurring_renewal_module_invoke($type, &$order, &$fee) {
  foreach (module_list() as $module) {
    $function = $module . '_' . $type;
    if (function_exists($function)) {
      $function($order, $fee);
    }
  }
}

/**
 * Implements hook_views_api().
 */
function uc_recurring_views_api() {
  return array(
    'api' => 2,
    'path' => drupal_get_path('module', 'uc_recurring') . '/views',
  );
}

Functions

Namesort descending Description
theme_uc_recurring_admin_table Displays a table for users to administer their recurring fees.
theme_uc_recurring_user_table Displays a table for users to administer their recurring fees.
uc_recurring_charge_profile Process a charge on a recurring profile.
uc_recurring_create_renewal_order Create a new order to be renewed via a recurring profile.
uc_recurring_cron Implementation of hook_cron().
uc_recurring_default_handler Default a recurring default handler, does nothing except returning TRUE.
uc_recurring_expire Process a fee expiration.
uc_recurring_extend_fee Handle extensions when a recurring payment was unsuccessful.
uc_recurring_fee_cancel Wrapper function to cancel a user's recurring fee.
uc_recurring_fee_status_label Returns the human readable labels for defined statuses.
uc_recurring_fee_user_delete Delete a recurring fee from a user.
uc_recurring_fee_user_load Loads a recurring fee from a user.
uc_recurring_fee_user_save Saves a recurring fee for a user.
uc_recurring_get_all_fees Get all fees is the system.
uc_recurring_get_extension Returns the time to extend for a payment attempt.
uc_recurring_get_extension_list Retuns a list of all the extensions for a specific recurring fee.
uc_recurring_get_fees Get an array of recurring fees associated with any product on an order.
uc_recurring_get_fees_for_expiration Get all pending fees that should be expired.
uc_recurring_get_fees_for_renew Get all pending fees that should be renewed.
uc_recurring_get_fee_ops Provide default options.
uc_recurring_get_recurring_info Get the recurring handlers info.
uc_recurring_get_user_fees Get an array of recurring fees associated with a user.
uc_recurring_includes Include uc recurring own payment gateway implementations.
uc_recurring_init Implementation of hook_init().
uc_recurring_invoke 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.
uc_recurring_menu Implementation of hook_menu().
uc_recurring_order Implementation of hook_order().
uc_recurring_payment_method_supported Checks that a payment method can process recurring fees. This is done by checking that a recurring fee handler exists for the payment method.
uc_recurring_perm Implementation of hook_perm().
uc_recurring_process_extensions Handle extensions when a recurring payment was unsuccessful.
uc_recurring_recurring_info Implementation of hook_recurring_info().
uc_recurring_renew Process a renewal, either from the cron job or manually from a fee handler.
uc_recurring_renewal_module_invoke Invokes uc_recurring renewal hooks in every module.
uc_recurring_save_extensions Save a set of extensions.
uc_recurring_set_intervals Set the intervals after a successful charge.
uc_recurring_theme Implementation of hook_theme().
uc_recurring_token_list Implementation of hook_token_list(). (token.module)
uc_recurring_token_values Implementation of hook_token_values(). (token.module)
uc_recurring_uc_checkout_complete Implementation of hook_uc_checkout_complete().
uc_recurring_uc_message Implementation of hook_uc_message().
uc_recurring_user Implementation of hook_user().
uc_recurring_user_access Restrict access to recurring fee operations for users.
uc_recurring_user_fees Display users recurring fees.
uc_recurring_views_api Implements hook_views_api().
_uc_recurring_get_recurring_info_handlers Get the data of the handlers, keyed by the fee handler.

Constants

Namesort descending Description
UC_RECURRING_ACCESS_ALLOW
UC_RECURRING_ACCESS_DENY Recurring access.
UC_RECURRING_ACCESS_IGNORE
UC_RECURRING_FEE_STATUS_ACTIVE Recurring fee states.
UC_RECURRING_FEE_STATUS_CANCELLED
UC_RECURRING_FEE_STATUS_EXPIRED
UC_RECURRING_FEE_STATUS_SUSPENDED
UC_RECURRING_MENU_DEFAULT
UC_RECURRING_MENU_DISABLED Shortcuts for defining disabled or default menu items in the recurring info definitions, rather then needing to specify the whole menu item.
UC_RECURRING_UNLIMITED_INTERVALS Unlimited number of intervals.