You are here

uc_recurring.module in Ubercart 5

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

File

payment/uc_recurring/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
 */

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

/**
 * Implementation of hook_menu().
 */
function uc_recurring_menu($may_cache) {
  $items = array();
  global $user;
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/store/orders/recurring',
      'title' => t('Recurring fees'),
      'description' => t('View the recurring fees on your orders.'),
      'callback' => 'uc_recurring_admin',
      'access' => user_access('administer recurring fees'),
      'type' => MENU_NORMAL_ITEM,
      'weight' => 5,
    );
  }
  else {
    if (arg(0) == 'user' && arg(2) == 'recurring' && intval(arg(3)) > 0) {
      $items[] = array(
        'path' => 'user/' . arg(1) . '/recurring/' . arg(3) . '/cancel',
        'title' => t('Cancel the recurring fee?'),
        'description' => t('Cancel a recurring fee.'),
        'callback' => 'drupal_get_form',
        'callback arguments' => array(
          'uc_recurring_user_cancel_form',
          arg(1),
          arg(3),
        ),
        'access' => $user->uid == intval(arg(1)) || user_access('administer recurring fees'),
        'type' => MENU_CALLBACK,
      );
    }
    if (arg(3) == 'recurring' && intval(arg(4)) > 0) {
      if (arg(5) == 'charge') {
        $items[] = array(
          'path' => 'admin/store/orders/recurring/' . arg(4) . '/charge',
          'title' => t('Charge recurring fee @fee?', array(
            '@fee' => arg(4),
          )),
          'callback' => 'drupal_get_form',
          'callback arguments' => array(
            'uc_recurring_admin_charge_form',
          ),
          'access' => user_access('administer recurring fees'),
          'type' => MENU_CALLBACK,
        );
      }
      elseif (arg(5) == 'edit') {
        $items[] = array(
          'path' => 'admin/store/orders/recurring/' . arg(4) . '/edit',
          'title' => t('Edit recurring fee @fee', array(
            '@fee' => arg(4),
          )),
          'callback' => 'drupal_get_form',
          'callback arguments' => array(
            'uc_recurring_admin_edit_form',
          ),
          'access' => user_access('administer recurring fees'),
          'type' => MENU_CALLBACK,
        );
      }
      elseif (arg(5) == 'delete') {
        $items[] = array(
          'path' => 'admin/store/orders/recurring/' . arg(4) . '/delete',
          'title' => t('Delete recurring fee @fee?', array(
            '@fee' => arg(4),
          )),
          'callback' => 'drupal_get_form',
          'callback arguments' => array(
            'uc_recurring_admin_delete_form',
          ),
          'access' => user_access('administer recurring fees'),
          'type' => MENU_CALLBACK,
        );
      }
    }
  }
  return $items;
}

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

/**
 * Implementation of hook_enable().
 */
function uc_recurring_enable() {

  // Get the weight of the Credit Card module.
  $weight = db_result(db_query("SELECT weight FROM {system} WHERE name = '%s'", 'uc_credit'));

  // Update the weight of the recurring module so its hooks get called after the
  // credit module.
  db_query("UPDATE {system} SET weight = %d WHERE name = '%s'", max(1000, $weight + 1), 'uc_recurring');
}

/**
 * Implementation of hook_form_alter().
 */
function uc_recurring_form_alter($form_id, &$form) {

  // We may need to alter the checkout form to remove invalid payment methods.
  if ($form_id == 'uc_cart_checkout_form' && isset($form['panes']['payment'])) {
    $order->products = uc_cart_get_contents();

    // Make no changes if no recurring fees are found.
    if (uc_recurring_find_fees($order) == array()) {
      return;
    }

    // If configured, display a message about the recurring fees.
    if ($message = variable_get('uc_recurring_checkout_message', '')) {
      drupal_set_message(check_markup($message));
    }

    // Remove invalid payment methods from the payment pane.
    $valid = variable_get('uc_recurring_payment_methods', array());
    foreach (array_keys($form['panes']['payment']['payment_method']['#options']) as $key) {
      if (!isset($valid[$key]) || $valid[$key] === 0) {
        unset($form['panes']['payment']['payment_method']['#options'][$key]);
      }
    }
    $count = count($form['panes']['payment']['payment_method']['#options']);
    if ($count == 0) {

      // Display an error message if no payment methods remain.
      drupal_set_message(t('There are no payment methods configured for orders with recurring fees!'), 'error');
      drupal_set_message(t('Please contact an administrator to solve the issue.'), 'error');
    }
    elseif ($count == 1) {

      // If only one payment method remains, make it the default.
      $form['panes']['payment']['payment_method']['#default_value'] = array_pop(array_keys($form['panes']['payment']['payment_method']['#options']));
    }
  }

  // Wipe any existing recurring fees on the review form load to prevent
  // duplicate or unexpected fees.
  if ($form_id == 'uc_cart_checkout_review_form') {
    db_query("DELETE FROM {uc_recurring_users} WHERE order_id = %d", $_SESSION['cart_order']);
  }
  if ($form_id == 'uc_order_view_update_form') {

    // Load the order object based on the form value for the order ID.
    $order = uc_order_load($form['order_id']['#value']);

    // Load up the valid payment methods.
    $methods = variable_get('uc_recurring_payment_methods', array());

    // Check to make sure the payment method is good and we're in CC debug mode.
    if ($methods[$order->payment_method] === $order->payment_method && variable_get('uc_credit_debug', FALSE)) {

      // Look for recurring fees on this order.
      $fees = uc_recurring_find_fees($order);

      // If we have fees, check to see if they've already been added to the order.
      if (count($fees)) {
        $result = db_result(db_query("SELECT COUNT(*) FROM {uc_recurring_users} WHERE order_id = %d AND fee_handler = '%s'", $order->order_id, variable_get('uc_recurring_handler', 'uc_recurring')));

        // If they haven't been added, display the checkbox to make it so.
        if ($result == 0) {
          $form['process_fees'] = array(
            '#type' => 'checkbox',
            '#title' => t('Process the recurring fees associated with products on this order.', array(
              '@count' => count($fees),
            )),
            '#description' => t('This action will not be available after any fees are successfully processed.<br /><b>Important:</b> You must verify that the credit card information is correct before processing the fees!'),
            '#weight' => 5,
          );
          $form['#submit']['uc_recurring_order_view_update_form_submit'] = array();
        }
      }
    }
  }
}

// Submit function for the order view update form to process recurring fees.
function uc_recurring_order_view_update_form_submit($form_id, $form_values) {
  if ($form_values['process_fees']) {
    $order = uc_order_load($form_values['order_id']);
    $fees = uc_recurring_find_fees($order);
    if (count($fees)) {
      $pass = TRUE;
      foreach ($fees as $fee) {
        if (!uc_recurring_process($order, $fee)) {
          uc_order_comment_save($order->order_id, 0, t('The recurring fee for product @model failed.', array(
            '@model' => $fee->model,
          )), 'admin', $order->order_status);
          $pass = FALSE;
        }
      }
      if ($pass == FALSE) {
        drupal_set_message(t('One or more recurring fees failed to process as indicated in the admin comments.'), 'error');
      }
    }
  }
}

/**
 * Implementation of hook_cron().
 */
function uc_recurring_cron() {
  $successes = 0;
  $fails = 0;

  // Look for any fees ready for a charge that are reference transactions
  // routed through the core recurring system.
  $result = db_query("SELECT * FROM {uc_recurring_users} WHERE fee_handler LIKE 'uc_recurring_byref:%%' AND remaining_intervals > 0 AND next_charge <= %d", time());
  while ($fee = db_fetch_array($result)) {
    $handler = explode(':', $fee['fee_handler']);
    $fee['data'] = unserialize($fee['data']);

    // Attempt to process the reference transaction.
    if (uc_recurring_charge($fee)) {

      // Update the fee in the database.
      if ($fee['remaining_intervals'] == 1) {
        $next_charge = time();
      }
      else {
        $next_charge = strtotime('+' . $fee['regular_interval'], $fee['next_charge']);
      }
      db_query("UPDATE {uc_recurring_users} SET next_charge = %d, remaining_intervals = remaining_intervals - 1, charged_intervals = charged_intervals + 1 WHERE rfid = %d", $next_charge, $fee['rfid']);
      $successes++;
    }
    else {
      $fails++;
    }
  }
  if ($successes > 0 || $fails > 0) {
    watchdog('uc_recurring', t('!successes recurring fees processed successfully; !fails failed.', array(
      '!successes' => $successes,
      '!fails' => $fails,
    )));
  }
}

/**
 * Implementation of hook_user().
 */
function uc_recurring_user($op, &$edit, &$account, $category = NULL) {
  global $user;
  switch ($op) {
    case 'view':
      if ($user->uid && ($user->uid == $account->uid || user_access('view all orders'))) {

        // Get a table of recurring fees associated with this user.
        $table = uc_recurring_user_table($account->uid);

        // If fees exist, display them in a table.
        if (!empty($table)) {
          $items['recurring_fees'] = array(
            'value' => $table,
            'class' => 'member',
          );
          return array(
            t('Recurring fees') => $items,
          );
        }
      }
  }
}

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

/**
 * Implementation of hook_order().
 */
function uc_recurring_order($op, &$arg1, $arg2) {
  switch ($op) {
    case 'submit':
      if (variable_get('uc_recurring_checkout_process', TRUE)) {
        $fees = uc_recurring_find_fees($arg1);
        if (count($fees)) {
          $pass = TRUE;
          $order = uc_order_load($arg1->order_id);
          foreach ($fees as $fee) {
            if (!uc_recurring_process($order, $fee)) {
              uc_order_comment_save($order->order_id, 0, t('The recurring fee for product @model failed.', array(
                '@model' => $fee->model,
              )), 'admin', $order->order_status);
              $pass = FALSE;
            }
          }
          if ($pass == FALSE) {
            $process = variable_get('uc_recurring_checkout_fail', 'fail');
            if ($process == 'fail' && uc_payment_balance($order) < $order->order_total) {
              $process = 'proceed';
            }
            switch ($process) {
              case 'fail':
                return array(
                  array(
                    'pass' => FALSE,
                    'message' => t('Your order cannot be completed, because we could not process your recurring payment. Please review your payment details and contact us to complete your order if the problem persists.'),
                  ),
                );
              case 'proceed':
                return array(
                  array(
                    'pass' => TRUE,
                    'message' => t('Your order has been submitted, but we may need to contact you to ensure your recurring fee is setup properly. Thank you for your understanding.'),
                  ),
                );
            }
          }
        }
      }
      break;
    case 'update':
      if (uc_order_status_data($arg1->order_status, 'state') == 'in_checkout') {
        db_query("UPDATE {uc_recurring_users} SET uid = %d WHERE uid = 0 AND order_id = %d", $arg1->uid, $arg1->order_id);
      }
  }
}

/**
 * Implementation of hook_product_feature().
 */
function uc_recurring_product_feature() {
  $features[] = array(
    'id' => 'recurring',
    'title' => t('Recurring fee'),
    'callback' => 'uc_recurring_feature_form',
    'delete' => 'uc_recurring_fee_delete',
    'settings' => 'uc_recurring_settings_form',
  );
  return $features;
}

/**
 * Implementation of hook_recurring_fee_ops().
 */
function uc_recurring_recurring_fee_ops($context, $fee) {
  $ops = array();
  switch ($context) {
    case 'fee_admin':
      if ($fee['remaining_intervals'] > 0) {
        $ops[] = l(t('charge'), 'admin/store/orders/recurring/' . $fee['rfid'] . '/charge');
      }
      $ops[] = l(t('edit'), 'admin/store/orders/recurring/' . $fee['rfid'] . '/edit');
      $ops[] = l(t('delete'), 'admin/store/orders/recurring/' . $fee['rfid'] . '/delete');
      break;
    case 'user':
      $ops[] = l(t('cancel'), 'user/' . $fee['uid'] . '/recurring/' . $fee['rfid'] . '/cancel');
  }
  return $ops;
}

/******************************************************************************
 * Workflow-ng Hooks                                                          *
 ******************************************************************************/

// Tell Workflow about the various order events.
function uc_recurring_event_info() {
  $events['fee_expires'] = array(
    '#label' => t('Recurring payment expires'),
    '#module' => t('Recurring Payments'),
    '#arguments' => array(
      'order' => array(
        '#entity' => 'order',
        '#lable' => t('Order'),
      ),
    ),
  );
  $events['fee_charge_successful'] = array(
    '#label' => t('Payment is charged successfully'),
    '#module' => t('Recurring Payments'),
    '#arguments' => array(
      'order' => array(
        '#entity' => 'order',
        '#lable' => t('Order'),
      ),
    ),
  );
  $events['fee_charge_fails'] = array(
    '#label' => t('Payment charge fails'),
    '#module' => t('Recurring Payments'),
    '#arguments' => array(
      'order' => array(
        '#entity' => 'order',
        '#lable' => t('Order'),
      ),
    ),
  );
  return $events;
}

/*******************************************************************************
 * Callback Functions
 ******************************************************************************/

// Builds the form to display for adding or editing a recurring fee.
function uc_recurring_feature_form($node, $feature) {
  drupal_add_css(drupal_get_path('module', 'uc_recurring') . '/uc_recurring.css');
  if (!empty($feature)) {
    $fee = uc_recurring_fee_load('product', $feature['pfid']);
  }
  $options = array(
    '' => t('<Any>'),
    $node->model => $node->model,
  );
  if (module_exists('uc_attribute')) {
    $result = db_query("SELECT model FROM {uc_product_adjustments} WHERE nid = %d", $node->nid);
    while ($row = db_fetch_object($result)) {
      if (!in_array($row->model, $options)) {
        $options[$row->model] = $row->model;
      }
    }
  }
  $form['model'] = array(
    '#type' => 'select',
    '#title' => t('Applicable Model/SKU'),
    '#description' => t('Select the applicable product model/SKU for this fee.'),
    '#options' => $options,
    '#default_value' => $fee['model'],
  );
  $form['fee_amount'] = array(
    '#type' => 'textfield',
    '#title' => t('Recurring fee amount'),
    '#description' => t('Charge this amount each billing period.<br />The product price is still charged at checkout.'),
    '#default_value' => $fee['fee_amount'],
    '#size' => 16,
    '#field_prefix' => variable_get('uc_sign_after_amount', FALSE) ? '' : variable_get('uc_currency_sign', '$'),
    '#field_suffix' => variable_get('uc_sign_after_amount', FALSE) ? variable_get('uc_currency_sign', '$') : '',
  );
  $form['initial'] = array(
    '#type' => 'fieldset',
    '#title' => t('Initial charge'),
    '#collapsible' => FALSE,
    '#description' => t('Specify the time to wait to start charging the recurring fee after checkout. Remember the product price will be charged at the time of checkout.'),
    '#attributes' => array(
      'class' => 'interval-fieldset',
    ),
  );
  $form['initial']['initial_charge_value'] = array(
    '#type' => 'select',
    '#options' => drupal_map_assoc(uc_range(0, 52)),
    '#default_value' => $fee['initial_charge_value'],
  );
  $form['initial']['initial_charge_unit'] = array(
    '#type' => 'select',
    '#options' => array(
      'days' => t('day(s)'),
      'weeks' => t('week(s)'),
      'months' => t('month(s)'),
      'years' => t('year(s)'),
    ),
    '#default_value' => $fee['initial_charge_unit'],
  );
  $form['regular'] = array(
    '#type' => 'fieldset',
    '#title' => t('Regular interval'),
    '#collapsible' => FALSE,
    '#description' => t('Specify the length of the billing period for this fee.'),
    '#attributes' => array(
      'class' => 'interval-fieldset',
    ),
  );
  $form['regular']['regular_interval_value'] = array(
    '#type' => 'select',
    '#options' => drupal_map_assoc(uc_range(1, 52)),
    '#default_value' => $fee['regular_interval_value'],
  );
  $form['regular']['regular_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' => $fee['regular_interval_unit'],
  );
  $form['number_intervals'] = array(
    '#type' => 'textfield',
    '#title' => t('Number of billing periods'),
    '#description' => t('Specify how many times the recurring fee will be charged.'),
    '#size' => 16,
    '#default_value' => $fee['number_intervals'],
    '#required' => TRUE,
  );
  return uc_product_feature_form($form);
}
function uc_recurring_feature_form_validate($form_id, $form_values) {
  if (intval($form_values['number_intervals']) <= 0) {
    form_set_error('number_intervals', t('Only positive whole number values are accepted for the number of billing periods.'));
  }
}
function uc_recurring_feature_form_submit($form_id, $form_values) {
  if (empty($form_values['pfid'])) {
    $fee['pfid'] = db_next_id('{uc_product_features}_pfid');
  }
  else {
    $fee['pfid'] = $form_values['pfid'];
  }
  $fee = array(
    'pfid' => $fee['pfid'],
    'model' => $form_values['model'],
    'fee_amount' => $form_values['fee_amount'],
    'initial_charge' => $form_values['initial_charge_value'] . ' ' . $form_values['initial_charge_unit'],
    'regular_interval' => $form_values['regular_interval_value'] . ' ' . $form_values['regular_interval_unit'],
    'number_intervals' => intval($form_values['number_intervals']),
  );
  uc_recurring_fee_save('product', $fee);
  $args = array(
    '@product' => empty($fee['model']) ? t('this product') : t('product @model', array(
      '@model' => $fee['model'],
    )),
    '!amount' => uc_currency_format($fee['fee_amount']),
    '!initial' => $fee['initial_charge'],
    '!regular' => $fee['regular_interval'],
    '!intervals' => t('!num times', array(
      '!num' => $fee['number_intervals'] - 1,
    )),
  );
  $data = array(
    'pfid' => $fee['pfid'],
    'nid' => $form_values['nid'],
    'fid' => 'recurring',
    'description' => t('When @product is purchased, add a fee for !amount charged first after !initial and every !regular after that !intervals.', $args),
  );
  return uc_product_feature_save($data);
}

// Adds the settings for the recurring module on the feature settings form.
function uc_recurring_settings_form() {
  $form['uc_recurring_handler'] = array(
    '#type' => 'select',
    '#title' => t('Recurring fee handler'),
    '#description' => t('Select a module to process recurring fees on your site.'),
    '#options' => drupal_map_assoc(module_implements('recurring_fee', TRUE)),
    '#default_value' => variable_get('uc_recurring_handler', 'uc_recurring'),
  );
  foreach (_payment_method_list() as $method) {
    $options[$method['id']] = $method['name'];
  }
  $form['uc_recurring_payment_methods'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Valid payment methods for orders with recurring fees'),
    '#description' => t('Only selected payment methods will be available for customers purchasing products with recurring fees.<br />It is up to you to make sure your chosen handler is compatible with the payment methods you select.<br />For example, the uc_recurring handler is only compatible with the Credit Card payment method.'),
    '#options' => $options,
    '#default_value' => variable_get('uc_recurring_payment_methods', array()),
  );
  $form['uc_recurring_checkout_message'] = array(
    '#type' => 'textarea',
    '#title' => t('Recurring free checkout form message'),
    '#description' => t('Enter a message to be displayed on the checkout form page when a customer has products in the cart with recurring fees.<br />Leave blank to not display any message.'),
    '#default_value' => variable_get('uc_recurring_checkout_message', ''),
  );
  $form['uc_recurring_checkout_process'] = array(
    '#type' => 'checkbox',
    '#title' => t('Attempt to process recurring fees during checkout.'),
    '#description' => t('If not selected, you must have an alternate way of processing fees.<br />With the default handler, this is only possible in credit card debug mode.'),
    '#default_value' => variable_get('uc_recurring_checkout_process', TRUE),
  );
  $form['uc_recurring_checkout_fail'] = array(
    '#type' => 'radios',
    '#title' => t('Action to take if a recurring fee fails to process during checkout'),
    '#description' => t('Regardless of your selection, an admin comment will report the failure.<br /><strong>Note:</strong> Even if you select the first option, checkout will complete if another payment has already been captured.'),
    '#options' => array(
      'fail' => t('Return a failed message and do not complete checkout.'),
      'proceed' => t('Return a failed message but complete checkout.'),
      'silent' => t('Show no message and complete checkout.'),
    ),
    '#default_value' => variable_get('uc_recurring_checkout_fail', 'fail'),
  );
  return $form;
}

// Displays a table for users to administer their recurring fees.
function uc_recurring_user_table($uid) {
  $rows = array();
  $output = '';

  // Setup a header array for the table.
  $header = array(
    t('Order'),
    t('Amount'),
    t('Interval'),
    t('Next charge'),
    t('Remaining'),
    t('Operations'),
  );

  // Loop through the fees sorted by the order ID descending.
  $result = db_query("SELECT * FROM {uc_recurring_users} WHERE uid = %d AND remaining_intervals > 0 ORDER BY order_id DESC", $uid);
  while ($fee = db_fetch_array($result)) {
    $ops = array();

    // Get the $ops from the module implementing the handler.
    $callback = $fee['fee_handler'] . '_recurring_fee_ops';
    if (function_exists($callback)) {
      $ops = $callback('user', $fee);
    }

    // Add the row to the table for display.
    $rows[] = array(
      l($fee['order_id'], 'user/' . $uid . '/order/' . $fee['order_id']),
      uc_currency_format($fee['fee_amount']),
      array(
        'data' => check_plain($fee['regular_interval']),
        'nowrap' => 'nowrap',
      ),
      $fee['remaining_intervals'] == 0 ? '-' : format_date($fee['next_charge'], 'small'),
      $fee['remaining_intervals'],
      array(
        'data' => implode(' ', $ops),
        'nowrap' => 'nowrap',
      ),
    );
  }

  // Only display the table if fees were found.
  if (count($rows) > 0) {
    $output = theme('table', $header, $rows);
  }
  return $output;
}

// Displays the confirm form for cancelling a recurring fee.
function uc_recurring_user_cancel_form($uid, $rfid) {
  $form['uid'] = array(
    '#type' => 'value',
    '#value' => $uid,
  );
  $form['rfid'] = array(
    '#type' => 'value',
    '#value' => $rfid,
  );
  return confirm_form($form, t('Are you sure you want to cancel your recurring fee?'), 'user/' . $uid, t('This action cannot be undone and may result in the termination of subscription services.'), t('Confirm'), t('Cancel'));
}
function uc_recurring_user_cancel_form_submit($form_id, $form_values) {
  uc_recurring_fee_cancel($form_values['rfid']);
  drupal_set_message(t('The recurring fee has been cancelled.'));
  return 'user/' . $form_values['uid'];
}

// Displays a table for the administration of recurring fees.
function uc_recurring_admin() {
  $output = drupal_get_form('uc_recurring_admin_filter_form');
  $header = array(
    array(
      'data' => t('ID'),
      'field' => 'ru.rfid',
      'sort' => 'desc',
    ),
    array(
      'data' => t('Order'),
      'field' => 'ru.order_id',
    ),
    array(
      'data' => t('Amount'),
      'field' => 'ru.fee_amount',
    ),
    array(
      'data' => t('Next'),
      'field' => 'ru.next_charge',
    ),
    array(
      'data' => t('Interval'),
      'field' => 'ru.regular_interval',
    ),
    array(
      'data' => t('Left'),
      'field' => 'ru.remaining_intervals',
    ),
    array(
      'data' => t('Total'),
    ),
    array(
      'data' => t('Operations'),
    ),
  );
  if (arg(4) == 'view' && intval(arg(6)) > 0) {
    if (arg(5) == 'fee') {
      $result = db_query("SELECT * FROM {uc_recurring_users} AS ru WHERE ru.rfid = %d", arg(6));
    }
    elseif (arg(5) == 'order') {
      $result = db_query("SELECT * FROM {uc_recurring_users} AS ru WHERE ru.order_id = %d", arg(6));
    }
  }
  else {
    $result = pager_query("SELECT * FROM {uc_recurring_users} AS ru" . tablesort_sql($header), 30);
  }
  while ($fee = db_fetch_array($result)) {
    $ops = array();

    // Get the $ops from the module implementing the handler.
    $callback = $fee['fee_handler'] . '_recurring_fee_ops';
    if (function_exists($callback)) {
      $ops = $callback('fee_admin', $fee);
    }
    $rows[] = array(
      l($fee['rfid'], 'admin/store/orders/recurring/view/fee/' . $fee['rfid']),
      l($fee['order_id'], 'admin/store/orders/' . $fee['order_id']),
      uc_currency_format($fee['fee_amount']),
      $fee['remaining_intervals'] == 0 ? '-' : format_date($fee['next_charge'], 'small'),
      array(
        'data' => check_plain($fee['regular_interval']),
        'nowrap' => 'nowrap',
      ),
      $fee['remaining_intervals'],
      $fee['remaining_intervals'] + $fee['charged_intervals'],
      array(
        'data' => implode(' ', $ops),
        'nowrap' => 'nowrap',
      ),
    );
  }
  $output .= theme('table', $header, $rows);
  $output .= theme('pager', NULL, 30, 0);
  if (arg(4) == 'view') {
    $output .= l(t('Back to the full list.'), 'admin/store/orders/recurring');
  }
  return $output;
}

// Filter by a specific order ID.
function uc_recurring_admin_filter_form() {
  $form['type'] = array(
    '#type' => 'select',
    '#options' => array(
      'order' => t('Order ID'),
      'fee' => t('Fee ID'),
    ),
    '#default_value' => arg(5) == 'fee' ? 'fee' : 'order',
    '#prefix' => '<div style="float: left; margin-right: 1em;">',
    '#suffix' => '</div>',
  );
  $form['id'] = array(
    '#type' => 'textfield',
    '#default_value' => arg(6),
    '#size' => 10,
    '#prefix' => '<div style="float: left; margin-right: 1em;">',
    '#suffix' => '</div>',
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Filter'),
    '#attributes' => array(
      'style' => 'margin: .85em 0em;',
    ),
  );
  return $form;
}
function uc_recurring_admin_filter_form_submit($form_id, $form_values) {
  if (intval($form_values['id']) > 0) {
    return 'admin/store/orders/recurring/view/' . $form_values['type'] . '/' . $form_values['id'];
  }
}

// Confirm a recurring fee charge.
function uc_recurring_admin_charge_form() {
  $fee = uc_recurring_fee_load('user', arg(4));
  $form['message'] = array(
    '#value' => '<div>' . t('Are you sure you want to charge the customer !amount at this time?', array(
      '!amount' => uc_currency_format($fee['fee_amount']),
    )) . '</div>',
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Charge'),
    '#suffix' => l(t('Cancel'), uc_referer_uri()),
  );
  return $form;
}
function uc_recurring_admin_charge_form_submit($form_id, $form_values) {
  $fee = uc_recurring_fee_load('user', arg(4));

  // Attempt to process the charge.
  if (uc_recurring_charge($fee)) {

    // Update the fee in the database.
    $next_charge = strtotime('+' . $fee['regular_interval']);
    db_query("UPDATE {uc_recurring_users} SET next_charge = %d, remaining_intervals = remaining_intervals - 1, charged_intervals = charged_intervals + 1 WHERE rfid = %d", $next_charge, $fee['rfid']);
    drupal_set_message(t('Recurring fee @fee charged successfully.', array(
      '@fee' => arg(4),
    )));
  }
  else {
    drupal_set_message(t('Attempt to charge recurring fee @fee failed.', array(
      '@fee' => arg(4),
    )), 'error');
  }
  return 'admin/store/orders/recurring/view/fee/' . arg(4);
}

// Let an admin edit a recurring fee.
function uc_recurring_admin_edit_form() {
  drupal_add_css(drupal_get_path('module', 'uc_recurring') . '/uc_recurring.css');
  $fee = uc_recurring_fee_load('user', arg(4));
  list($fee['regular_interval_value'], $fee['regular_interval_unit']) = explode(' ', $fee['regular_interval']);
  $form['fee_amount'] = array(
    '#type' => 'textfield',
    '#title' => t('Recurring fee amount'),
    '#description' => t('Charge this amount each billing period.'),
    '#default_value' => $fee['fee_amount'],
    '#size' => 16,
    '#field_prefix' => variable_get('uc_sign_after_amount', FALSE) ? '' : variable_get('uc_currency_sign', '$'),
    '#field_suffix' => variable_get('uc_sign_after_amount', FALSE) ? variable_get('uc_currency_sign', '$') : '',
  );
  $form['remaining_intervals'] = array(
    '#type' => 'textfield',
    '#title' => t('Remaining billing periods'),
    '#description' => t('Specify how many more times to charge the fee.'),
    '#size' => 16,
    '#default_value' => $fee['remaining_intervals'],
  );
  $form['regular'] = array(
    '#type' => 'fieldset',
    '#title' => t('Regular interval'),
    '#collapsible' => FALSE,
    '#description' => t('Modify the length of the billing period for this fee. Changing this value will reset the timer for the next charge. You can also charge the fee manually to collect payment ahead of time and reset the interval.'),
    '#attributes' => array(
      'class' => 'interval-fieldset',
    ),
  );
  $form['regular']['regular_interval_value'] = array(
    '#type' => 'select',
    '#options' => drupal_map_assoc(uc_range(1, 52)),
    '#default_value' => $fee['regular_interval_value'],
  );
  $form['regular']['regular_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' => $fee['regular_interval_unit'],
  );
  $form['reset_next_charge'] = array(
    '#type' => 'checkbox',
    '#title' => t('Reset the next charge timer upon form submission using the specified interval.'),
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
    '#suffix' => l(t('Cancel'), uc_referer_uri()),
  );
  return $form;
}
function uc_recurring_admin_edit_form_submit($form_id, $form_values) {
  $interval = $form_values['regular_interval_value'] . ' ' . $form_values['regular_interval_unit'];
  db_query("UPDATE {uc_recurring_users} SET fee_amount = %f, regular_interval = '%s', " . "remaining_intervals = %d WHERE rfid = %d", $form_values['fee_amount'], $interval, $form_values['remaining_intervals'], arg(4));
  if ($form_values['reset_next_charge']) {
    $next_charge = strtotime('+' . $interval);
    db_query("UPDATE {uc_recurring_users} SET next_charge = %d WHERE rfid = %d", $next_charge, arg(4));
  }
  drupal_set_message(t('The changes to the fee have been saved.'));
  return 'admin/store/orders/recurring/view/fee/' . arg(4);
}

// Confirm a recurring fee deletion.
function uc_recurring_admin_delete_form() {
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Delete'),
    '#suffix' => l(t('Cancel'), uc_referer_uri()),
  );
  return $form;
}
function uc_recurring_admin_delete_form_submit($form_id, $form_values) {
  uc_recurring_fee_delete(array(
    'pfid' => arg(4),
  ), 'user');
  drupal_set_message(t('Recurring fee @fee deleted.', array(
    '@fee' => arg(4),
  )));
  return 'admin/store/orders/recurring';
}

/**
 * Saves a reference transaction fee to the database.
 *
 * Recurring handlers for payment gateways processing recurring fees through a
 * reference transaction should use the uc_recurring_byref system.  By calling
 * this function from a recurring fee handler and passing on the order and fee
 * objects as received, the system will add the fee to the single table that
 * will be processed on cron.
 *
 * @param $order
 *   The order object for the recurring fee.
 * @param $fee
 *   The fee data object.
 * @param $handler
 *   The name of the recurring fee handler using the uc_recurring_byref system.
 * @param $callback
 *   The name of the charge function for the gateway that will be used to charge
 *     the reference transactions.
 * @param $ref_id
 *   The reference ID that will be used to process the reference transaction at
 *     the payment gateway.
 */
function uc_recurring_byref_save($order, $fee, $handler, $callback, $ref_id) {

  // Build the data array stored for the fee.
  $data = array(
    'charge_callback' => $callback,
    'ref_id' => $ref_id,
    'model' => $fee->model,
  );
  $fee = array(
    'rfid' => db_next_id('{uc_product_users}_rfid'),
    'uid' => $order->uid,
    'fee_handler' => 'uc_recurring_byref:' . $handler,
    'next_charge' => strtotime('+' . $fee->initial_charge),
    'fee_amount' => $fee->fee_amount,
    'regular_interval' => $fee->regular_interval,
    'remaining_intervals' => $fee->number_intervals,
    'charged_intervals' => 0,
    'order_id' => $order->order_id,
    'data' => serialize($data),
  );
  uc_recurring_fee_save('user', $fee);
  uc_order_comment_save($order->order_id, 0, t('Recurring fee <a href="!url">!fee</a> added to order.', array(
    '!url' => url('admin/store/orders/recurring/view/fee/' . $fee['rfid']),
    '!fee' => $fee['rfid'],
  )));
  return TRUE;
}

/**
 * Saves a recurring fee either for a product or for a user.
 */
function uc_recurring_fee_save($type, $data) {
  switch ($type) {
    case 'product':

      // First attempt to update an existing row.
      db_query("UPDATE {uc_recurring_products} SET model = '%s', fee_amount = %f, initial_charge = '%s', regular_interval = '%s', number_intervals = %d WHERE pfid = %d", $data['model'], $data['fee_amount'], $data['initial_charge'], $data['regular_interval'], $data['number_intervals'], $data['pfid']);

      // Otherwise insert this feature as a new row.
      if (db_affected_rows() == 0) {
        db_query("INSERT INTO {uc_recurring_products} (pfid, model, fee_amount, initial_charge, regular_interval, number_intervals) VALUES (%d, '%s', %f, '%s', '%s', %d)", $data['pfid'], $data['model'], $data['fee_amount'], $data['initial_charge'], $data['regular_interval'], $data['number_intervals']);
      }
      break;
    case 'user':

      // First attempt to update an existing row.
      db_query("UPDATE {uc_recurring_users} SET uid = %d, fee_handler = '%s', next_charge = %d, fee_amount = %f, regular_interval = '%s', remaining_intervals = %d, charged_intervals = %d, order_id = %d, data = '%s' WHERE rfid = %d", $data['uid'], $data['fee_handler'], $data['next_charge'], $data['fee_amount'], $data['regular_interval'], $data['remaining_intervals'], $data['charged_intervals'], $data['order_id'], $data['data'], $data['rfid']);

      // Otherwise insert this feature as a new row.
      if (db_affected_rows() == 0) {
        db_query("INSERT INTO {uc_recurring_users} (rfid, uid, fee_handler, next_charge, fee_amount, regular_interval, remaining_intervals, charged_intervals, order_id, data, created) VALUES (%d, %d, '%s', %d, %f, '%s', %d, %d, %d, '%s', %d)", $data['rfid'], $data['uid'], $data['fee_handler'], $data['next_charge'], $data['fee_amount'], $data['regular_interval'], $data['remaining_intervals'], $data['charged_intervals'], $data['order_id'], $data['data'], time());
      }
      break;
  }
}

/**
 * Loads a recurring fee either from a product or for a user.
 *
 * @param $type
 *   'product' to load a recurring fee product feature.
 *   'user' to load a recurring fee schedule for a user.
 * @param $id
 *   The ID of the fee to load, either the product feature ID or the recurring
 *     fee ID from the appropriate table.
 * @return
 *   An associative array of data for the specified fee.
 */
function uc_recurring_fee_load($type, $id) {
  switch ($type) {
    case 'product':
      $fee = db_fetch_array(db_query("SELECT * FROM {uc_recurring_products} WHERE pfid = %d", $id));
      if (!empty($fee)) {
        list($fee['initial_charge_value'], $fee['initial_charge_unit']) = explode(' ', $fee['initial_charge']);
        list($fee['regular_interval_value'], $fee['regular_interval_unit']) = explode(' ', $fee['regular_interval']);
      }
      break;
    case 'user':
      $fee = db_fetch_array(db_query("SELECT * FROM {uc_recurring_users} WHERE rfid = %d", $id));
      if ($fee['fee_handler'] == 'uc_recurring') {
        $fee['data'] = unserialize($fee['data']);
        if ($key = uc_credit_encryption_key()) {
          $crypt = new uc_encryption_class();
          $fee['data']['payment_details']['cc_number'] = $crypt
            ->decrypt($key, $fee['data']['payment_details']['cc_number']);
          if (variable_get('uc_credit_debug', FALSE)) {
            $fee['data']['payment_details']['cc_cvv'] = $crypt
              ->decrypt($key, $fee['data']['payment_details']['cc_cvv']);
          }
          $fee['data']['payment_details']['cc_exp_month'] = $crypt
            ->decrypt($key, $fee['data']['payment_details']['cc_exp_month']);
          $fee['data']['payment_details']['cc_exp_year'] = $crypt
            ->decrypt($key, $fee['data']['payment_details']['cc_exp_year']);
          uc_store_encryption_errors($crypt, 'uc_recurring');
        }
      }
      break;
  }
  return $fee;
}

/**
 * Deletes a recurring fee from a product or user.
 *
 * @param $type
 *   Either 'product' or 'user' to specify what type of delete needs to happen.
 * @param $id
 *   The ID of the recurring fee to be removed from the appropriate table.
 */
function uc_recurring_fee_delete($feature, $type = 'product') {
  switch ($type) {
    case 'product':
      db_query("DELETE FROM {uc_recurring_products} WHERE pfid = %d", $feature['pfid']);
      break;
    case 'user':
      module_invoke_all('recurring_api', 'delete', $feature['fid']);
      db_query("DELETE FROM {uc_recurring_users} WHERE rfid = %d", $feature['pfid']);
      break;
  }
}

/**
 * Cancels a user's recurring fee by setting remaining intervals to 0.
 *
 * @param $rfid
 *   The recurring fee's ID.
 */
function uc_recurring_fee_cancel($rfid) {
  db_query("UPDATE {uc_recurring_users} SET remaining_intervals = 0 WHERE rfid = %d", $rfid);
}

/**
 * Returns an array of recurring fees associated with any product on an order.
 *
 * @param $order
 *   The order object in question.
 * @return
 *   An array of recurring fee objects containing all their data from the DB.
 */
function uc_recurring_find_fees($order) {
  if (!is_array($order->products) || count($order->products) == 0) {
    return array();
  }
  $models = array();
  $nids = array();
  foreach ((array) $order->products as $product) {
    $nids[] = $product->nid;
    $models[] = check_plain($product->model);
  }
  $fees = array();
  $result = db_query("SELECT rp.*, nid FROM {uc_recurring_products} AS rp LEFT JOIN {uc_product_features} AS pf ON rp.pfid = pf.pfid WHERE rp.model IN ('" . implode("', '", $models) . "') OR (rp.model = '' AND pf.nid IN ('" . implode("', '", $nids) . "'))");
  while ($fee = db_fetch_object($result)) {
    $fees[] = $fee;
  }
  return $fees;
}

/**
 * Passes the information onto the specified fee handler for processing.
 *
 * @param $order
 *   The order object the fees are attached to.
 * @param $fee
 *   The fee object to be processed.
 * @return
 *   TRUE or FALSE indicating whether or not the processing was successful.
 */
function uc_recurring_process($order, $fee) {
  $handler = variable_get('uc_recurring_handler', 'uc_recurring') . '_recurring_fee';
  if (!function_exists($handler)) {
    drupal_set_message(t('The handler for processing recurring fees cannot be found.'), 'error');
    return FALSE;
  }
  if ($handler($order, $fee) == TRUE) {
    return TRUE;
  }
  return FALSE;
}

// Processes credit cards for the default handler.
function uc_recurring_charge($fee) {
  static $show = TRUE;

  // Get the charge function for the handler's credit card gateway.
  $func = $fee['data']['charge_callback'];

  // Whoa... bad function? ABORT! ABORT!
  if (!function_exists($func)) {
    if ($show) {
      watchdog('uc_recurring', t('Recurring payments failed to process due to invalid credit card gateway.'), WATCHDOG_ERROR);
      $show = FALSE;
    }
    return FALSE;
  }

  // Build the data array for the request.
  $data = array(
    'txn_type' => UC_CREDIT_REFERENCE_TXN,
    'ref_id' => $fee['data']['ref_id'],
  );

  // Run the charge.
  $result = $func($fee['order_id'], $fee['fee_amount'], $data);

  // Handle the result.
  if ($result['success'] === TRUE) {
    uc_payment_enter($fee['order_id'], 'credit', $fee['fee_amount'], 0, $result['data'], t('Recurring fee payment.') . '<br />' . $result['comment']);
    uc_order_comment_save($fee['order_id'], 0, t('!amount recurring fee collected for @model. (ID: <a href="!url">!fee</a>)', array(
      '!url' => url('admin/store/orders/recurring/view/fee/' . $fee['rfid']),
      '!fee' => $fee['rfid'],
      '!amount' => uc_currency_format($fee['fee_amount']),
      '@model' => $fee['data']['model'],
    )));

    // Modules can hook into the charge process using hook_recurring_api().
    module_invoke_all('recurring_api', 'charge', $fee);

    // Provide a couple Workflow events for folks to hook into.
    workflow_ng_invoke_event('fee_charge_successful', uc_order_load($fee['order_id']));
    if ($fee['remaining_intervals'] == 1) {
      workflow_ng_invoke_event('fee_expires', uc_order_load($fee['order_id']));
    }
  }
  else {
    uc_order_comment_save($fee['order_id'], 0, t('Error: Recurring fee <a href="!url">!fee</a> for product @model failed.', array(
      '!url' => url('admin/store/orders/recurring/view/fee/' . $fee['rfid']),
      '!fee' => $fee['rfid'],
      '@model' => $fee['data']['model'],
    )));
    watchdog('uc_recurring', t('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']));

    // Modules can hook into the charge process using hook_recurring_api().
    module_invoke_all('recurring_api', 'fail', $fee);

    // Provide a Workflow event for folks to hook into.
    workflow_ng_invoke_event('fee_charge_fails', uc_order_load($fee['order_id']));
  }
  return $result['success'];
}

Functions

Namesort descending Description
uc_recurring_admin
uc_recurring_admin_charge_form
uc_recurring_admin_charge_form_submit
uc_recurring_admin_delete_form
uc_recurring_admin_delete_form_submit
uc_recurring_admin_edit_form
uc_recurring_admin_edit_form_submit
uc_recurring_admin_filter_form
uc_recurring_admin_filter_form_submit
uc_recurring_byref_save Saves a reference transaction fee to the database.
uc_recurring_charge
uc_recurring_cron Implementation of hook_cron().
uc_recurring_enable Implementation of hook_enable().
uc_recurring_event_info
uc_recurring_feature_form
uc_recurring_feature_form_submit
uc_recurring_feature_form_validate
uc_recurring_fee_cancel Cancels a user's recurring fee by setting remaining intervals to 0.
uc_recurring_fee_delete Deletes a recurring fee from a product or user.
uc_recurring_fee_load Loads a recurring fee either from a product or for a user.
uc_recurring_fee_save Saves a recurring fee either for a product or for a user.
uc_recurring_find_fees Returns an array of recurring fees associated with any product on an order.
uc_recurring_form_alter Implementation of hook_form_alter().
uc_recurring_menu Implementation of hook_menu().
uc_recurring_order Implementation of hook_order().
uc_recurring_order_view_update_form_submit
uc_recurring_perm Implementation of hook_perm().
uc_recurring_process Passes the information onto the specified fee handler for processing.
uc_recurring_product_feature Implementation of hook_product_feature().
uc_recurring_recurring_fee_ops Implementation of hook_recurring_fee_ops().
uc_recurring_settings_form
uc_recurring_user Implementation of hook_user().
uc_recurring_user_cancel_form
uc_recurring_user_cancel_form_submit
uc_recurring_user_table