You are here

uc_recurring_product.module in UC Recurring Payments and Subscriptions 6.2

Same filename and directory in other branches
  1. 7.2 modules/uc_recurring_product/uc_recurring_product.module

Add recurring payments/fees to a product. This is imlpemented through Ubercarts product features.

Development: Chris Hood http://univate.com.au

File

modules/uc_recurring_product/uc_recurring_product.module
View source
<?php

/**
 * @file
 * Add recurring payments/fees to a product. This is imlpemented through
 * Ubercarts product features.
 *
 * Development:
 *   Chris Hood http://univate.com.au
 */

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

/**
 * Builds the form to display adding or editing a recurring fee feature.
 */
function uc_recurring_product_feature_form($form_state, $node, $feature) {
  drupal_add_css(drupal_get_path('module', 'uc_recurring') . '/uc_recurring.css');
  drupal_add_js(drupal_get_path('module', 'uc_recurring') . '/uc_recurring.js', 'module');
  if (!empty($feature)) {
    $product = uc_recurring_product_fee_load($feature['pfid']);
  }
  $options = uc_product_get_models($node);
  $form['nid'] = array(
    '#type' => 'hidden',
    '#value' => $node->nid,
  );
  $form['model'] = array(
    '#type' => 'select',
    '#title' => t('Applicable SKU'),
    '#description' => t('Select the applicable product model/SKU for this fee.'),
    '#options' => $options,
    '#default_value' => $product->model,
  );
  $form['fee'] = array(
    '#type' => 'fieldset',
    '#title' => t('Recurring Fee Amount'),
    '#collapsible' => FALSE,
    '#description' => t('Specify the amount that is charged on each renewal date.'),
  );
  $attributes = array();
  if ($product->fee_amount == 0) {
    $attributes['checked'] = 'checked';
  }
  $form['fee']['fee_same_product'] = array(
    '#type' => 'checkbox',
    '#title' => t('Set the recurring fee amount to the same as selling price of the product at the time of purchase.'),
    '#attributes' => $attributes,
  );
  $form['fee']['product_price'] = array(
    '#type' => 'hidden',
    '#value' => $node->sell_price,
  );
  $form['fee']['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' => $product->fee_amount == 0 ? $node->sell_price : $product->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', '$') : '',
    '#attributes' => $product->fee_amount == 0 ? array(
      'disabled' => 'disabled',
    ) : array(),
  );
  $form['interval'] = array(
    '#type' => 'fieldset',
    '#title' => t('Payment Interval Settings'),
    '#collapsible' => FALSE,
    '#description' => t('Remember the product price will be charged at the time of checkout. This section specifies when the recurring amount will be charged.'),
  );
  $form['interval']['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.'),
    '#attributes' => array(
      'class' => 'interval-fieldset',
    ),
  );
  $form['interval']['initial']['initial_charge_value'] = array(
    '#type' => 'select',
    '#options' => drupal_map_assoc(uc_range(0, 52)),
    '#default_value' => $product->initial_charge_value,
  );
  $form['interval']['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' => $product->initial_charge_unit,
  );
  $form['interval']['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['interval']['regular']['regular_interval_value'] = array(
    '#type' => 'select',
    '#options' => drupal_map_assoc(uc_range(1, 52)),
    '#default_value' => $product->regular_interval_value,
  );
  $form['interval']['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' => $product->regular_interval_unit,
  );
  $form['num_interval'] = array(
    '#type' => 'fieldset',
    '#title' => t('Number of billing periods'),
    '#collapsible' => FALSE,
    '#description' => t('Specify how many times the recurring fee will be charged.'),
  );
  $attributes = array();
  if ($product->number_intervals < 0) {
    $attributes['checked'] = 'checked';
  }
  $form['num_interval']['unlimited_intervals'] = array(
    '#type' => 'checkbox',
    '#title' => t('Unlimited rebillings.'),
    '#attributes' => $attributes,
  );
  $form['num_interval']['number_intervals'] = array(
    '#type' => 'textfield',
    '#title' => t('Number of billing periods'),
    '#size' => 16,
    '#default_value' => $product->number_intervals < 0 ? '' : $product->number_intervals,
    '#attributes' => $product->number_intervals < 0 ? array(
      'disabled' => 'disabled',
    ) : array(),
  );
  return uc_product_feature_form($form);
}
function uc_recurring_product_feature_form_validate($form, &$form_state) {
  if (empty($form_state['values']['unlimited_intervals']) && intval($form_state['values']['number_intervals']) < 0) {
    form_set_error('number_intervals', t('Only positive whole number values are accepted for the number of billing periods.'));
  }
}

/**
 * Submit handler for the recurring feature.
 */
function uc_recurring_product_feature_form_submit($form, &$form_state) {

  // Use the form specified pfid if available.
  if (!empty($form_state['values']['pfid'])) {
    $pfid = $form_state['values']['pfid'];
  }

  // Build the recurring fee's product object.
  $product->pfid = $pfid;
  $product->model = $form_state['values']['model'];
  $product->fee_amount = $form_state['values']['fee_amount'];
  $product->initial_charge = $form_state['values']['initial_charge_value'] . ' ' . $form_state['values']['initial_charge_unit'];
  $product->regular_interval = $form_state['values']['regular_interval_value'] . ' ' . $form_state['values']['regular_interval_unit'];

  // If number intervals is negative, it means that it's unlimited intervals.
  $product->number_intervals = empty($form_state['values']['unlimited_intervals']) ? $form_state['values']['number_intervals'] : UC_RECURRING_UNLIMITED_INTERVALS;
  $product->nid = $form_state['values']['nid'];
  $form_state['redirect'] = uc_recurring_product_feature_save($product);
}

/**
 *
 */
function uc_recurring_product_feature_save(&$product) {
  $context = array(
    'revision' => 'formatted-original',
    'location' => 'recurring-feature-submit',
  );
  $args = array(
    '@product' => empty($product->model) ? t('this product') : t('@model of this product', array(
      '@model' => $product->model,
    )),
    '@amount' => empty($product->fee_amount) ? t('the same amount as the product selling price') : uc_price($product->fee_amount, $context),
    '@initial' => $product->initial_charge,
    '@regular' => $product->regular_interval,
    '@intervals' => t('@num times', array(
      '@num' => $product->number_intervals < 0 ? t('unlimited') : $product->number_intervals - 1,
    )),
  );

  // Build the feature's data array.
  $data = array(
    'pfid' => $product->pfid,
    'nid' => $product->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),
  );

  // Save the product feature and store the returned URL as our redirect.
  $redirect = uc_product_feature_save($data);
  if (empty($product->pfid)) {
    $product->pfid = db_last_insert_id('uc_product_features', 'pfid');
  }
  uc_recurring_product_fee_product_save($product);
  return $redirect;
}

/**
 * Adds the settings for the recurring module on the feature settings form.
 */
function uc_recurring_product_settings_form() {
  $form['message'] = array(
    '#value' => '<p>' . t('Settings for recurring payments can now be found under the <a href="@settings">Payment Settings</a>', array(
      '@settings' => url('admin/store/settings/payment/edit/recurring'),
    )) . '</p>',
  );
  return $form;
}

/**
 * Submit handler for the processing recurring fee.
 */
function uc_recurring_product_order_view_update_form_submit($form, &$form_state) {
  $order = uc_order_load($form_state['values']['order_id']);
  uc_recurring_product_process_order($order);
}

/**
 * Implementation of hook_form_FORM-ID_alter().
 *
 * @see uc_cart_checkout_form()
 */
function uc_recurring_product_form_uc_cart_checkout_form_alter(&$form, $form_state) {

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

    // Make no changes if no recurring fees are found.
    if (uc_recurring_product_get_recurring_products_in_order($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());
    if (!empty($form['panes']['payment']['payment_method']['#options'])) {
      foreach (array_keys($form['panes']['payment']['payment_method']['#options']) as $key) {
        if (isset($valid[$key]) && $valid[$key] === 0 || !uc_recurring_payment_method_supported($key)) {
          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.
        if (user_access('administer recurring fees')) {
          drupal_set_message(t('There are no payment methods configured for orders with recurring fees, enable one from <a href="@url">recurring fee admin settings</a>.', array(
            '@url' => url('admin/store/settings/products/edit/features'),
          )), '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']));
      }
    }
  }
}

/**
 * Implementation of hook_form_FORM-ID_alter().
 *
 * @see uc_order_view_update_form()
 */
function uc_recurring_product_form_uc_order_view_update_form_alter(&$form, $form_state) {

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

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

  // If they haven't been added, display the checkbox to make it so.
  if (count($products)) {
    $form['process_fees'] = array(
      '#type' => 'checkbox',
      '#title' => t('Process the @count recurring fees associated with products on this order.', array(
        '@count' => count($products),
      )),
      '#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_product_order_view_update_form_submit';
  }
}

/**
 * Implementation of hook_order().
 */
function uc_recurring_product_order($op, $arg1, $arg2) {
  switch ($op) {

    // TODO: Allow admin to create a recurring order from "create order" page.
    case 'submit':
      if (variable_get('uc_recurring_checkout_process', TRUE)) {
        if (uc_recurring_product_process_order($arg1) === FALSE) {
          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.'),
            ),
          );
        }
      }
      break;
  }
}

/**
 * Passes the information onto the specified fee handler for processing.
 *
 * @param $order
 *   The order object the fees are attached to.
 * @param $data
 *   Optional; Data that should be added to the fee object.
 * @return
 *   FALSE on failure or array with new recurring fee IDs.
 */
function uc_recurring_product_process_order($order, $data = array()) {
  global $user;

  // Get all the products that should have a recurring fee created for them.
  if ($products = uc_recurring_product_get_recurring_products_in_order($order)) {

    // Check we have an handler to deal with the recurring payment.
    $payment_method = !empty($order->payment_method) ? $order->payment_method : 'default';
    if (!($fee_handler = uc_recurring_get_recurring_info($payment_method))) {
      drupal_set_message(t('A handler for processing and renewing recurring fees cannot be found for the @payment-method payment method.', array(
        '@payment-method' => $order->payment_method,
      )), 'error');
      return FALSE;
    }
    $return = array();

    // Create a new fee object.
    $fee_template = new stdClass();
    $fee_template->uid = $order->uid;
    $fee_template->fee_handler = $fee_handler['fee handler'];
    $fee_template->created = time();
    $fee_template->order_id = $order->order_id;
    $fee_template->module = 'uc_recurring_product';

    // Iterate over the products that require a fee.
    foreach ($products as $product) {
      $fee = drupal_clone($fee_template);
      $product_fee = $product['recurring product'];
      $order_product_id = $product['product']->order_product_id;

      // If the product fee amount is 0, it means we need to use the product
      // price. This allows recurring fees to be adjusted by attributes.
      $fee->fee_amount = $product_fee->fee_amount == 0 ? $product['product']->price : $product_fee->fee_amount;
      $fee->fee_amount *= $product['product']->qty;

      // Add the product's title as the order title.
      $fee->fee_title = t('Renewal of product @title', array(
        '@title' => $product['product']->title,
      ));
      $fee->next_charge = strtotime('+' . $product_fee->initial_charge);
      $fee->initial_charge = $product_fee->initial_charge;
      $fee->regular_interval = $product_fee->regular_interval;
      $fee->remaining_intervals = $product_fee->number_intervals;
      $fee->charged_intervals = 0;
      $fee->data = array(
        'model' => $product_fee->model,
        'nid' => $product_fee->nid,
        'qty' => $product['product']->qty,
        'extension' => 0,
      ) + $data;
      $fee->attempts = 0;
      $fee->pfid = $product_fee->pfid;
      $fee->order_product_id = $order_product_id;
      $fee->own_handler = !empty($fee_handler['own handler']);
      drupal_alter('recurring_fee_user_create', $fee);

      // Let the implementing module process.
      if (uc_recurring_invoke($fee->fee_handler, 'process callback', array(
        $order,
        &$fee,
      ))) {

        // Recurring processing was successful, get the fee.
        // We will save all fees together after we are sure all of them were
        // processed properly.
        $fee_objects[] = $fee;
      }
      else {

        // We have an error, so break. No fee object was saved.
        return FALSE;
      }
    }
    if (!empty($fee_objects)) {

      // There was no error, so save all fee objects.
      foreach ($fee_objects as $object) {
        $rfid = uc_recurring_fee_user_save($object);
        uc_order_comment_save($order->order_id, $user->uid, t('Recurring fee <a href="@recurring-view-fee">@rfid</a> added to order.', array(
          '@recurring-view-fee' => url('admin/store/orders/recurring/view/fee/' . $rfid),
          '@rfid' => $rfid,
        )));
        $return[] = $rfid;
      }
    }
  }
  return $return;
}

/**
 * On renewal we need to add a product to the order that matches the recurring fee.
 *
 * @param $order
 *   Order Object.
 * @param $fee
 *   Recurring fee Object.
 */
function uc_recurring_product_recurring_renewal_pending(&$order, &$fee) {
  if ($fee->module != 'uc_recurring_product') {
    return;
  }

  // Set a single product - the recurring fee.
  $product = new stdClass();
  $product->order_id = $new_id;
  $product->nid = $fee->data['nid'];
  $product->model = $fee->data['model'];
  $product->title = !empty($fee->fee_title) ? $fee->fee_title : t('Renewal of product @model', array(
    '@model' => $product->model,
  ));
  $product->qty = !empty($fee->data['qty']) ? $fee->data['qty'] : 1;
  $product->price = $fee->fee_amount / $product->qty;

  // initialize these items to remove warnings
  $product->cost = 0;
  $product->manufacturer = '';
  $product->weight = 0;

  // Add a flag that this order is a recurring fee order, so it won't be
  // processed by uc_recurring_product_process_order().
  $product->data['recurring_fee'] = TRUE;
  $order->products[] = $product;
}

/**
 * Get an array of recurring products that should be created for an order.
 *
 * Unlike uc_recurring_get_fees(), this functions checks for products in an
 * order that might not be submitted, thus a recurring fee record hasn't been
 * created yet.
 *
 * @param $order
 *   The order object.
 * @return
 *  An array with the products and their product fee objects.
 */
function uc_recurring_product_get_recurring_products_in_order($order) {
  $return = array();
  $products = array();

  // The product node IDs that might be reccuring products.
  $nids = array();
  if (!empty($order->products)) {
    $fees = uc_recurring_get_fees($order);
    foreach ($order->products as $value) {
      $processed = FALSE;
      foreach ($fees as $fee) {
        if ($fee->order_product_id == $value->order_product_id) {
          $processed = TRUE;
        }
      }
      if ($processed) {
        continue;
      }

      // Don't process new orders that were created by uc_recurring_renew().
      if (empty($value->data['recurring_fee'])) {

        // Get all the models of all products.
        $products[$value->nid][] = array(
          'model' => $value->model,
          'product' => $value,
        );
        $nids[] = $value->nid;
      }
    }
    if ($products) {

      // Get recurring products according to the products node IDs in the order.
      $result = db_query("SELECT p.pfid, p.model, p.fee_amount, p.initial_charge, p.regular_interval, p.number_intervals, f.nid\n                          FROM {uc_recurring_product} p\n                            LEFT JOIN {uc_product_features} f ON p.pfid=f.pfid\n                          WHERE f.nid IN (" . db_placeholders($nids) . ")", $nids);
      while ($row = db_fetch_object($result)) {
        foreach ($products[$row->nid] as $key => $product) {

          // No model name indicates we should work for all models.
          // However, we still need to have the sku of the recurring product,
          // otherwise recurring orders will show products without a sku.
          if ($row->model == '') {
            $row->model = $product['model'];
          }
          if ($row->model == $product['model']) {
            $return[] = $product + array(
              'recurring product' => $row,
            );
          }
        }
      }
    }
  }
  return $return;
}

/**
 * Saves a recurring product.
 *
 * @param $product
 *   A recurring product object.
 */
function uc_recurring_product_fee_product_save($product) {

  // Allow other modules to change the saved data.
  drupal_alter('recurring_fee_product_save', $product);

  // Delete existing record.
  db_query("DELETE FROM {uc_recurring_product} WHERE pfid = %d", $product->pfid);
  drupal_write_record('uc_recurring_product', $product);
}

/**
 * Loads a recurring fee from a product fee ID.
 *
 * @param $pifd
 *   The product fee ID to load.
 * @return
 *   The product fee object.
 */
function uc_recurring_product_fee_load($pfid) {
  $result = db_query("SELECT pfid, model, fee_amount, initial_charge, regular_interval, number_intervals\n                      FROM {uc_recurring_product}\n                      WHERE pfid = %d", $pfid);
  $product = db_fetch_object($result);
  list($product->initial_charge_value, $product->initial_charge_unit) = explode(' ', $product->initial_charge);
  list($product->regular_interval_value, $product->regular_interval_unit) = explode(' ', $product->regular_interval);

  // Allow other module to alter the loaded object.
  drupal_alter('recurring_fee_product_load', $product);
  return $product;
}

/**
 * Deletes a recurring product.
 *
 * @param $pfid
 *   The ID of the recurring fee to be removed from the appropriate table.
 */
function uc_recurring_product_fee_product_delete($pfid) {
  module_invoke_all('recurring_product_delete', $pfid);
  db_query("DELETE FROM {uc_recurring_product} WHERE pfid = %d", $pfid);
}

Functions

Namesort descending Description
uc_recurring_product_feature Implementation of hook_product_feature().
uc_recurring_product_feature_form Builds the form to display adding or editing a recurring fee feature.
uc_recurring_product_feature_form_submit Submit handler for the recurring feature.
uc_recurring_product_feature_form_validate
uc_recurring_product_feature_save
uc_recurring_product_fee_load Loads a recurring fee from a product fee ID.
uc_recurring_product_fee_product_delete Deletes a recurring product.
uc_recurring_product_fee_product_save Saves a recurring product.
uc_recurring_product_form_uc_cart_checkout_form_alter Implementation of hook_form_FORM-ID_alter().
uc_recurring_product_form_uc_order_view_update_form_alter Implementation of hook_form_FORM-ID_alter().
uc_recurring_product_get_recurring_products_in_order Get an array of recurring products that should be created for an order.
uc_recurring_product_order Implementation of hook_order().
uc_recurring_product_order_view_update_form_submit Submit handler for the processing recurring fee.
uc_recurring_product_process_order Passes the information onto the specified fee handler for processing.
uc_recurring_product_recurring_renewal_pending On renewal we need to add a product to the order that matches the recurring fee.
uc_recurring_product_settings_form Adds the settings for the recurring module on the feature settings form.