You are here

uc_coupon.module in Ubercart Discount Coupons 7.2

Same filename and directory in other branches
  1. 5 uc_coupon.module
  2. 6 uc_coupon.module
  3. 7.3 uc_coupon.module

File

uc_coupon.module
View source
<?php

/**
 * @file
 * Provides discount coupons for Ubercart.
 *
 * Version: 2.x
 * Drupal Core: 7.x
 * Ubercart Core: 3.x
 *
 * Original code by Blake Lucchesi (www.boldsource.com)
 * Maintained by David Long (dave@longwaveconsulting.com) and Chris Oden (wodenx@gmail.com)
 *
 * Send any suggestions and feedback to the above address.
 */
require_once 'uc_coupon.entity.inc';

/**
 * Implements hook_menu().
 */
function uc_coupon_menu() {
  $items = array();
  $items['admin/store/coupons'] = array(
    'title' => 'Coupons',
    'description' => 'Manage store discount coupons.',
    'page callback' => 'uc_coupon_display',
    'page arguments' => array(
      'active',
    ),
    'access arguments' => array(
      'view store coupons',
    ),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'uc_coupon.admin.inc',
  );
  $items['admin/store/coupons/list'] = array(
    'title' => 'Active coupons',
    'description' => 'View active coupons.',
    'page callback' => 'uc_coupon_display',
    'page arguments' => array(
      'active',
    ),
    'access arguments' => array(
      'view store coupons',
    ),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'uc_coupon.admin.inc',
    'weight' => 0,
  );
  $items['admin/store/coupons/inactive'] = array(
    'title' => 'Inactive coupons',
    'description' => 'View inactive coupons.',
    'page callback' => 'uc_coupon_display',
    'page arguments' => array(
      'inactive',
    ),
    'access arguments' => array(
      'view store coupons',
    ),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'uc_coupon.admin.inc',
    'weight' => 1,
  );
  $items['admin/store/coupons/add'] = array(
    'title' => 'Add new coupon',
    'description' => 'Add a new coupon.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'uc_coupon_add_form',
    ),
    'access arguments' => array(
      'manage store coupons',
    ),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'uc_coupon.admin.inc',
    'weight' => 2,
  );
  $items['admin/store/coupons/%uc_coupon'] = array(
    'title callback' => 'uc_coupon_title',
    'title arguments' => array(
      3,
    ),
    'description' => 'View coupon details.',
    'page callback' => 'uc_coupon_view',
    'page arguments' => array(
      3,
    ),
    'access arguments' => array(
      'view store coupons',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'uc_coupon.admin.inc',
    'weight' => 3,
  );
  $items['admin/store/coupons/%uc_coupon/view'] = array(
    'title' => 'View',
    'description' => 'View coupon details.',
    'access arguments' => array(
      'view store coupons',
    ),
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'file' => 'uc_coupon.admin.inc',
    'weight' => 0,
  );
  $items['admin/store/coupons/%uc_coupon/print'] = array(
    'title' => 'Print',
    'description' => 'Print coupon.',
    'page callback' => 'uc_coupon_print',
    'page arguments' => array(
      3,
      5,
      'print',
    ),
    'access arguments' => array(
      'view store coupons',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'uc_coupon.admin.inc',
    'weight' => 1,
  );
  $items['admin/store/coupons/%uc_coupon/edit'] = array(
    'title' => 'Edit',
    'description' => 'Edit an existing coupon.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'uc_coupon_add_form',
      3,
    ),
    'access arguments' => array(
      'manage store coupons',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'uc_coupon.admin.inc',
    'weight' => 2,
  );
  $items['admin/store/coupons/%uc_coupon/delete'] = array(
    'title' => 'Delete',
    'description' => 'Delete a coupon.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'uc_coupon_delete_confirm',
      3,
    ),
    'access arguments' => array(
      'manage store coupons',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'uc_coupon.admin.inc',
    'weight' => 3,
  );
  $items['admin/store/coupons/%uc_coupon/codes'] = array(
    'title' => 'Download bulk coupon codes',
    'description' => 'Download the list of bulk coupon codes as a CSV file.',
    'page callback' => 'uc_coupon_codes_csv',
    'page arguments' => array(
      3,
    ),
    'access arguments' => array(
      'view store coupons',
    ),
    'file' => 'uc_coupon.admin.inc',
    'type' => MENU_CALLBACK,
  );
  $items['admin/store/coupons/autocomplete/node'] = array(
    'title' => 'Node autocomplete',
    'page callback' => 'uc_coupon_autocomplete_node',
    'access arguments' => array(
      'manage store coupons',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'uc_coupon.admin.inc',
  );
  $items['admin/store/coupons/autocomplete/term'] = array(
    'title' => 'Term autocomplete',
    'page callback' => 'uc_coupon_autocomplete_term',
    'access arguments' => array(
      'manage store coupons',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'uc_coupon.admin.inc',
  );
  $items['admin/store/coupons/autocomplete/user'] = array(
    'title' => 'User autocomplete',
    'page callback' => 'uc_coupon_autocomplete_user',
    'access arguments' => array(
      'manage store coupons',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'uc_coupon.admin.inc',
  );
  $items['admin/store/coupons/autocomplete/role'] = array(
    'title' => 'Role autocomplete',
    'page callback' => 'uc_coupon_autocomplete_role',
    'access arguments' => array(
      'manage store coupons',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'uc_coupon.admin.inc',
  );
  $items['admin/store/settings/coupon'] = array(
    'title' => 'Coupon module settings',
    'description' => 'Configure the discount coupon module settings.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'uc_coupon_settings_form',
    ),
    'access arguments' => array(
      'administer store',
    ),
    'file' => 'uc_coupon.admin.inc',
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/store/reports/coupon'] = array(
    'title' => 'Coupon usage reports',
    'description' => 'View coupon usage reports.',
    'page callback' => 'uc_coupon_reports',
    'access arguments' => array(
      'view reports',
    ),
    'file' => 'uc_coupon.reports.inc',
    'type' => MENU_NORMAL_ITEM,
  );
  return $items;
}

/**
 * Properly handle %uc_coupon wildcard.
 * (Necessary to prevent PHP runtime notice.)
 */
function uc_coupon_to_arg($arg) {
  return $arg;
}

/**
 * Title callback for coupon print preview.
 */
function uc_coupon_title($coupon) {
  return $coupon->name;
}

/**
 * Implements hook_permission().
 */
function uc_coupon_permission() {
  $perms = array(
    'view store coupons' => array(
      'title' => t('view store coupons'),
      'description' => t('Display information about discount coupons.'),
    ),
    'manage store coupons' => array(
      'title' => t('manage store coupons'),
      'description' => t('Create, edit and delete discoutn coupons.'),
    ),
  );
  if (!module_exists('uc_reports')) {
    $perms['view reports'] = array(
      'title' => t('view reports'),
      'description' => t('Display coupon usage reports.'),
    );
  }
  return $perms;
}

/**
 * Implements hook_init().
 */
function uc_coupon_init() {
  global $conf;
  $conf['i18n_variables'][] = 'uc_coupon_pane_description';

  // Auto apply coupon from query string, if configured.
  if ($param = variable_get('uc_coupon_querystring', '')) {
    if (isset($_GET[$param]) && $_GET[$param]) {

      // We retain the querystring coupon so that it will validate if/when appropriate conditions are met.
      uc_coupon_session_add($_GET[$param], 'retain');
    }
  }
}

/**
 * Implements hook_theme().
 */
function uc_coupon_theme() {
  return array(
    'uc_coupon_automatic_discounts' => array(
      'render element' => 'form',
    ),
    'uc_coupon_form' => array(
      'render element' => 'form',
    ),
    'uc_coupon_actions' => array(
      'variables' => array(
        'coupon' => NULL,
      ),
      'file' => 'uc_coupon.admin.inc',
    ),
    'uc_coupon_code' => array(
      'variables' => array(
        'coupon' => NULL,
      ),
      'file' => 'uc_coupon.admin.inc',
    ),
    'uc_coupon_discount' => array(
      'variables' => array(
        'coupon' => NULL,
        'currency' => TRUE,
      ),
    ),
    'uc_coupon_certificate' => array(
      'variables' => array(
        'coupon' => NULL,
        'code' => NULL,
      ),
      'template' => 'uc-coupon-certificate',
      'path' => drupal_get_path('module', 'uc_coupon') . '/theme',
    ),
    'uc_coupon_page' => array(
      'variables' => array(
        'content' => NULL,
      ),
      'template' => 'uc-coupon-page',
      'path' => drupal_get_path('module', 'uc_coupon') . '/theme',
    ),
  );
}

/**
 * Implements hook_theme_registry_alter().
 */
function uc_coupon_theme_registry_alter(&$registry) {

  // Override the default theme for the cart block content - but only if not already overridden.
  if ($registry['uc_cart_block_content']['function'] == 'theme_uc_cart_block_content') {
    $registry['uc_cart_block_content']['function'] = 'uc_coupon_theme_uc_cart_block_content';
  }
}

/**
 * Count usage of a coupon.
 *
 * @param $cid
 *   The coupon id to count.
 * @param $uid
 *   (optional) The user id to count. Defaults to the current user.
 *
 * @return
 *   An associative array containing:
 *   - codes: An associative array of code => usage count.
 *   - user: The usage count by the specified (or current) user.
 */
function uc_coupon_count_usage($cid, $uid = NULL) {
  global $user;
  $weight = uc_order_status_data(variable_get('uc_coupon_used_order_status', 'processing'), 'weight');
  $usage = array(
    'codes' => array(),
  );
  $result = db_query("SELECT uco.code, COUNT(*) AS uses FROM {uc_coupons_orders} AS uco\n    LEFT JOIN {uc_orders} AS uo ON uco.oid = uo.order_id\n    LEFT JOIN {uc_order_statuses} AS uos ON uo.order_status = uos.order_status_id\n    WHERE uos.weight >= :weight AND uco.cid = :cid GROUP BY uco.code", array(
    ':weight' => $weight,
    ':cid' => $cid,
  ));
  foreach ($result as $row) {
    $usage['codes'][$row->code] = $row->uses;
  }
  if (is_null($uid)) {
    $uid = $user->uid;
  }
  $usage['user'] = db_query("SELECT COUNT(*) FROM {uc_coupons_orders} AS uco\n    LEFT JOIN {uc_orders} AS uo ON uco.oid = uo.order_id\n    LEFT JOIN {uc_order_statuses} AS uos ON uo.order_status = uos.order_status_id\n    WHERE uos.weight >= :weight AND uco.cid = :cid AND uo.uid = :uid", array(
    ':weight' => $weight,
    ':cid' => $cid,
    ':uid' => $uid,
  ))
    ->fetchField();

  // Allow other modules to implement usage counts.
  drupal_alter('uc_coupon_usage', $usage, $cid, $uid);
  return $usage;
}

/**
 * Theme for a coupon discount.
 * @param $variables
 *   'coupon' => The coupon whose discount is to be themed.
 *   'currency' => TRUE to include currency symbols.
 */
function theme_uc_coupon_discount($variables) {
  $coupon = $variables['coupon'];
  $currency = isset($variables['currency']) ? $variables['currency'] : TRUE;
  return _uc_coupon_format_discount($coupon, $currency);
}

/**
 * Format a coupon's value depending on the type, optionally including currency symbols.
 */
function _uc_coupon_format_discount($coupon, $currency = TRUE) {
  switch ($coupon->type) {
    case 'price':
      return $currency ? uc_currency_format($coupon->value) : $coupon->value;
    case 'percentage':
      return (double) $coupon->value . '%';
    case 'set_price':
      return '=' . ($currency ? uc_currency_format($coupon->value) : $coupon->value);
  }
}

/**
 * Generate a single bulk coupon code.
 */
function uc_coupon_get_bulk_code($coupon, $id) {

  // If this coupon has been validated, then $coupon->code is already a bulk code.
  if (isset($coupon->valid)) {
    $prefix = drupal_substr($coupon->code, 0, strlen($coupon->code) - $coupon->data['bulk_length']);
  }
  else {
    $prefix = $coupon->code;
  }
  $id = str_pad(dechex($id), strlen(dechex($coupon->data['bulk_number'])), '0', STR_PAD_LEFT);
  $length = strlen($prefix) + $coupon->data['bulk_length'];
  return strtoupper(substr($prefix . $id . md5($coupon->bulk_seed . $id), 0, $length));
}

/**
 * Load a coupon (single or bulk) from the supplied code.
 * @param $code
 *     The coupon code to search for.
 * @param $reset
 *    If TRUE the cache of codes for this request will be purged.  Any function which modifies
 *    a coupon should purge the cache.
 */
function uc_coupon_find($code, $reset = FALSE) {

  // This is expensive and can be called many times during coupon processing, so we
  // use a simple static cache.
  static $cached = array();
  if ($reset) {
    $cached = array();
  }
  if (!$code) {
    return FALSE;
  }
  elseif (array_key_exists($code, $cached)) {
    return $cached[$code];
  }

  // Look for matching single coupon first.
  $coupon = db_query("SELECT cid FROM {uc_coupons}\n    WHERE code = :code AND status = 1 AND bulk = 0 AND valid_from < :now AND (valid_until = 0 OR valid_until > :now)", array(
    ':code' => $code,
    ':now' => REQUEST_TIME,
  ))
    ->fetchObject();
  if ($coupon) {
    $cached[$code] = uc_coupon_load($coupon->cid);
    return $cached[$code];
  }

  // Look through bulk coupons.
  $result = db_query("SELECT cid, code, data, bulk_seed FROM {uc_coupons}\n    WHERE status = 1 AND bulk = 1 AND valid_from < :now AND (valid_until = 0 OR valid_until > :now)", array(
    ':now' => REQUEST_TIME,
  ));
  foreach ($result as $coupon) {

    // Check coupon prefix.
    $prefix_length = strlen($coupon->code);
    if (substr($code, 0, $prefix_length) != $coupon->code) {
      continue;
    }
    if ($coupon->data) {
      $coupon->data = unserialize($coupon->data);
    }

    // Check coupon sequence ID.
    $id = substr($code, $prefix_length, strlen(dechex($coupon->data['bulk_number'])));
    if (!preg_match("/^[0-9A-F]+\$/", $id)) {
      continue;
    }
    $id = hexdec($id);
    if ($id < 0 || $id > $coupon->data['bulk_number']) {
      continue;
    }

    // Check complete coupon code.
    if ($code == uc_coupon_get_bulk_code($coupon, $id)) {
      $cached[$code] = uc_coupon_load($coupon->cid);
      return $cached[$code];
    }
  }
  $cached[$code] = FALSE;
  return $cached[$code];
}

/**
 * Adds or updates a coupon code for the current session.
 *
 * @param $code
 *     The code to add or update.
 * @param $op
 *     Specifies the way the code should be handled the next time session codes are validated.
 *     - 'submit' - If the code fails validation it is removed from the session; otherwise it is retained.
 *       A success or failure message is displayed.  This is the default operation performed when
 *       a customer enters a coupon code manually.
 *     - 'retain' - The code remains in the session whether or not it passes validation.  A success
 *       message is displayed the first time the coupon passes. This is useful for
 *       codes which are added automatically in response to events occurring before any products
 *       have been added to the cart (e.g. via the querystring).
 *     - 'auto' - The code is removed from the session whether or not it passes validation.  However, if
 *       it does validate, the corresponding coupon will be considered valid for the current request only.
 *       This is useful for modules implementing hook_uc_coupon_revalidate(), which can decide whether or not
 *       to add their codes each time the valid coupon cache is rebuilt (e.g. automatic discounts based on
 *       additional conditions).
 */
function uc_coupon_session_add($code, $op = 'submit') {
  if (!variable_get('uc_coupon_allow_multiple', FALSE)) {
    $_SESSION['uc_coupons'] = array(
      $code => $op,
    );
  }
  else {
    $_SESSION['uc_coupons'][$code] = $op;
  }
}

/**
 * Removes one (or all) coupon codes from the session.
 *
 * @param $code
 *		The code to remove, or NULL to remove all codes.
 * @param $is_update
 * 		TRUE (the default) if removing this code represents an update of the session; that is, if the code
 *		was previously validated. FALSE otherwise (e.g. for removal of automatic discounts).  Ignored if
 *		a specific code is not specified.		
 */
function uc_coupon_session_clear($code = NULL) {
  if (isset($_SESSION)) {
    if (isset($code)) {
      unset($_SESSION['uc_coupons'][$code]);
    }
    else {
      unset($_SESSION['uc_coupons']);
    }
  }
}

/**
 * Checks to see if a given code is present in the session, or returns an associative array
 * of all codes in the session.
 *
 * @param $code
 *     (optional) The code to chec for.  If not specified, will return all codes.
 *
 * @return
 *     If a code is specified, returns TRUE if that code exists in the session, FALSE otherwise.  If no
 *     code is specified, returns an array of the form $code=>$op for all codes in the session.
 */
function uc_coupon_session_get($code = NULL) {
  if (isset($code)) {
    return isset($_SESSION['uc_coupons'][$code]);
  }
  elseif (isset($_SESSION['uc_coupons'])) {
    return $_SESSION['uc_coupons'];
  }
  else {
    return array();
  }
}

/**
 * Validates all coupons in the current session.  The validated coupons are statically
 * cached for each request.  The cache is rebuilt the first time this function is called,
 * or every time the cart contents are rebuilt.
 *
 * @param $products
 *     An array of products, in which case the cached list of valid coupons is rebuilt by
 *     revalidating all the codes in the session against that list.
 *
 *     NOTE: Forcing the cache to be rebuilt by calling this function with ($products != NULL)
 *     is discouraged.  Instead, you should rebuild the current cart contents
 *     by calling uc_cart_get_contents(NULL, 'rebuild').  This will ensure that the valid
 *     coupons list and the shopping cart are in sync.
 *
 * @return
 *     An array of fully validated coupon objects, indexed by code.
 */
function uc_coupon_session_validate($products = NULL) {
  static $valids = NULL;

  // If a list of products is specified, then rebuild the list.
  if (isset($products)) {
    $valids = array();

    // Allow modules an opportunity to add or remove coupons from the session.
    module_invoke_all('uc_coupon_revalidate', $products);

    // Fetch all codes in the session.
    $session = uc_coupon_session_get();
    if (!empty($session)) {

      // Process all coupons in the session.
      global $user;
      $order = new UcOrder();
      $order->products = $products;
      $order->data = array(
        'coupons' => array(),
      );
      foreach ($session as $code => $op) {
        $coupon = uc_coupon_validate($code, $order, $user);
        if ($coupon->valid) {

          // Process valid coupons.
          $valids[$code] = $coupon;
          $order->data['coupons'][$code] = $coupon->discounts;
          switch ($op) {
            case 'submit':
            case 'retain':

              // For coupons which were not valid (new submissions) we notify user and modules.
              drupal_set_message($coupon->message);
              module_invoke_all('uc_coupon_apply', $coupon);

              // And we mark them for revalidation.
              uc_coupon_session_add($code, 'revalidate');
              break;
            case 'auto':

              // Automatic coupons are never added to the session.
              uc_coupon_session_clear($code);
              break;
          }
        }
        else {

          // Process invalid coupons.
          switch ($op) {
            case 'submit':

              // For new coupon submissions, just issue an error and remove from session.
              drupal_set_message($coupon->message, 'error');
              uc_coupon_session_clear($code);
              break;
            case 'revalidate':
              if (!empty($products)) {

                // Only issue a message if the cart is not empty.
                drupal_set_message(t('%title is no longer applicable to your order', array(
                  '%title' => $coupon->title,
                )));
              }
              module_invoke_all('uc_coupon_remove', $coupon);

              // Keep code in the session in case it becomes valid again.
              uc_coupon_session_add($code, 'retain');
              break;
            case 'auto':

              // Automatic coupons are never added to the session.
              uc_coupon_session_clear($code);
              break;
          }
        }
      }
    }
  }
  elseif (!isset($valids)) {
    uc_cart_get_contents();
  }
  return $valids;
}

/**
 * Validate a coupon, and optionally calculate the order discount.
 *
 * @param $code
 *   The coupon code entered at the checkout screen.
 * @param $order
 *   The order that the coupon is being applied to.
 *   If NULL, the current cart contents will be used.
 *   If FALSE, product and order validation will be bypassed.
 * @param $account
 *   The user who is attempting to use the coupon.
 *   If NULL, the current user will be assumed.
 *   If FALSE, user validation will be bypassed.
 *
 * @return
 *   A coupon object with extended information about the validation:
 *   - $coupon->valid: TRUE if the code was valid, FALSE otherwise.
 *   - $coupon->code: The specific code to be applied (even for bulk coupons).
 *   - $coupon->title: The line item title for the discount.
 *   - $coupon->message: A message to be displayed accepting the acceptance or rejection of this coupon.
 *   - $coupon->amount: If $order !== FALSE, the discount that should be applied.
 *   - $coupon->discounts: if $order !== FALSE, an array discounts on individual products indexed by nid,
 *      containing the following fields:
 *       -> 'discount' = The full value of the discount on that item.
 *       -> 'pretax_discount' => The actual pre-tax discount. For fixed discounts to products with
 *          taxes included, we apply the face value of the coupon tax-inclusively also; that is,
 *          the actual discount is calculated so that the face value is correct after taxes.
 */
function uc_coupon_validate($code, $order = NULL, $account = NULL) {
  global $user;
  if (is_null($order)) {
    $order = new stdClass();
    $order->products = uc_cart_get_contents();
  }
  if (is_null($account)) {
    $account = $user;
  }

  // Look for an active coupon matching the code.
  $code = trim(strtoupper($code));
  $coupon = uc_coupon_find($code);
  if (!$coupon) {
    $coupon = new stdClass();
    $coupon->valid = FALSE;
    $coupon->message = t('This coupon code is invalid or has expired.');
    $coupon->title = t('Unknown');
    return $coupon;
  }

  // Assume the coupon is valid, unless a validation hook fails.
  $coupon->code = $code;
  $coupon->valid = TRUE;
  $coupon->usage = uc_coupon_count_usage($coupon->cid, $account ? $account->uid : NULL);
  unset($coupon->message);

  // Calculate the discount.
  if ($order) {
    $coupon->amount = 0;
    $coupon->pretax_amount = 0;
    $discounts = uc_coupon_calculate_discounts($coupon, $order, TRUE);
    if (!is_array($discounts)) {
      $coupon->discounts = array();
      $coupon->valid = FALSE;
      $coupon->message = $discounts;
    }
    else {
      $coupon->discounts = $discounts;
      foreach ($coupon->discounts as $item) {
        $coupon->amount += $item->discount;
        $coupon->pretax_amount += isset($item->pretax_discount) ? $item->pretax_discount : $item->discount;
      }
      $coupon->amount = round($coupon->amount, variable_get('uc_currency_prec', 2));
    }
  }

  // Invoke validation hook.
  foreach (module_implements('uc_coupon_validate') as $module) {
    $callback = $module . '_uc_coupon_validate';
    $result = $callback($coupon, $order, $account);
    if ($result === TRUE) {

      // This module wishes the coupon to be accepted.
      $coupon->valid = TRUE;
    }
    elseif (!is_null($result)) {

      // This module wishes the coupon to be rejected.
      $coupon->valid = FALSE;
      $coupon->message = $result;
    }
  }

  // Create the line item title for this coupon.
  $format = !empty($coupon->data['line_item_format']) ? $coupon->data['line_item_format'] : variable_get('uc_coupon_line_item_format', t('Coupon !code', array(
    '!code' => '[uc_coupon:code]',
  )));
  $coupon->title = token_replace(check_plain($format), array(
    'uc_coupon' => $coupon,
  ));

  // Create a success message.
  if ($coupon->valid && !isset($coupon->message)) {
    if (isset($coupon->data['apply_message'])) {
      $coupon->message = token_replace(check_plain($coupon->data['apply_message']), array(
        'uc_coupon' => $coupon,
      ));
    }
    else {
      $amount = theme('uc_price', array(
        'price' => $coupon->amount,
      ));
      if (isset($order) || variable_get('uc_coupon_show_in_cart', TRUE)) {
        $coupon->message = t('A discount of !amount has been applied to your order.', array(
          '!amount' => $amount,
        ));
      }
      else {
        $coupon->message = t('A discount of !amount will be applied at checkout.', array(
          '!amount' => $amount,
        ));
      }
    }
  }
  return $coupon;
}

/**
 * Implements hook_uc_coupon_validate().
 *
 * @param $coupon
 *   The coupon object to validate, with special fields set as follows:
 *   - $coupon->code: The specific code to be applied (even for bulk coupons).
 *   - $coupon->amount: If $order !== FALSE, the discount that should be applied.
 *   - $coupon->usage: Coupon usage data from uc_coupon_count_usage().
 * @param $order
 *   The order against which this coupon is to be applied, or FALSE to bypass order validation.
 * @param $account
 *   The account of the user trying to use the coupon, or FALSE to bypass user validation.
 *
 * @return
 *   TRUE if the coupon should be accepted.
 *   NULL to allow other modules to determine validation.
 *   Otherwise, a string describing the reason for failure.
 */
function uc_coupon_uc_coupon_validate(&$coupon, $order, $account) {

  // Check for allowed combinations.
  if (!empty($order->data['coupons'])) {
    foreach (array_keys($order->data['coupons']) as $code) {
      $other = uc_coupon_find($code);
      $other_listed = !empty($coupon->data['combinations']) && in_array($other->cid, $coupon->data['combinations']);
      $this_ok = (isset($coupon->data['negate_combinations']) xor $other_listed);
      $this_listed = !empty($other->data['combinations']) && in_array($coupon->cid, $other->data['combinations']);
      $other_ok = (isset($other->data['negate_combinations']) xor $this_listed);
      if (!$this_ok || !$other_ok) {
        return t('This coupon combination is not allowed.');
      }
    }
  }

  // Check maximum usage per code.
  if ($coupon->max_uses > 0 && !empty($coupon->usage['codes'][$coupon->code]) && $coupon->usage['codes'][$coupon->code] >= $coupon->max_uses) {
    return t('This coupon has reached the maximum redemption limit.');
  }

  // Check maximum usage per user.
  if ($account && isset($coupon->data['max_uses_per_user']) && $coupon->usage['user'] >= $coupon->data['max_uses_per_user']) {
    return t('This coupon has reached the maximum redemption limit.');
  }

  // Check user ID.
  if ($account && isset($coupon->data['users'])) {
    if (in_array("{$account->uid}", $coupon->data['users'], TRUE) xor !isset($coupon->data['negate_users'])) {
      return t('Your user ID is not allowed to use this coupon.');
    }
  }

  // Check roles.
  if ($account && isset($coupon->data['roles'])) {
    $role_found = FALSE;
    foreach ($coupon->data['roles'] as $role) {
      if (in_array($role, $account->roles)) {
        $role_found = TRUE;
        break;
      }
    }
    if ($role_found xor !isset($coupon->data['negate_roles'])) {
      return t('You do not have the correct permission to use this coupon.');
    }
  }
}

/**
 * Find items that a coupon will apply to and calculate the discounts.
 * @param $coupon
 *     A coupon object to apply, or a coupon code as a string.
 * @param $order
 *     The order object to which the coupon should be applied.
 */
function uc_coupon_calculate_discounts($coupon, $order) {
  if (!is_object($coupon)) {

    // If argument is a code, load the corresponding coupon.
    $coupon = uc_coupon_find($coupon);
  }

  // Discover if any items match the restrictions, and which items the discount should be calculated against.
  $restricted = isset($coupon->data['products']) || isset($coupon->data['skus']) || isset($coupon->data['terms']) || isset($coupon->data['product_types']);
  $matched = 0;
  $matched_price = 0;
  $total_qty = 0;
  $total_price = 0;
  $items = array();
  foreach ($order->products as $item) {
    if (isset($item->module) && $item->module == 'uc_coupon') {
      continue;
    }
    $node = node_load($item->nid);
    $qty = $item->qty;
    if (!$restricted) {

      // Coupons with no restrictions apply to all products.
      $include = TRUE;
    }
    else {

      // Other coupons only apply to matching products.
      $include = FALSE;
      $terms = _uc_coupon_list_terms($node);
      if (isset($coupon->data['products']) && isset($item->data['kit_id'])) {

        // Items that are part of product kits must be included or excluded all together, so we pre-empt other restrictions.
        $include = (isset($coupon->data['negate_products']) xor in_array($item->data['kit_id'], $coupon->data['products']));
      }
      else {
        if (isset($coupon->data['products']) && (isset($coupon->data['negate_products']) xor in_array($item->nid, $coupon->data['products']))) {
          $include = TRUE;
        }
        elseif (isset($coupon->data['products']) && isset($coupon->data['negate_products']) && in_array($item->nid, $coupon->data['products'])) {

          // always exclude if in list of negated products
        }
        elseif (isset($coupon->data['terms']) && (isset($coupon->data['negate_terms']) xor count(array_intersect($terms, $coupon->data['terms'])))) {
          $include = TRUE;
        }
        elseif (isset($coupon->data['terms']) && isset($coupon->data['negate_terms']) && count(array_intersect($terms, $coupon->data['terms']))) {

          // always exclude if one of the terms is in the list of negated terms
        }
        elseif (isset($coupon->data['skus']) && _uc_coupon_match_sku($item->model, $coupon->data['skus'])) {
          $include = TRUE;
        }
        elseif (isset($coupon->data['product_types']) && in_array($node->type, $coupon->data['product_types'])) {
          $include = TRUE;
        }
      }
    }

    // A matching product was found.
    if ($include) {
      $matched += $qty;
      $matched_price += $item->price * $qty;
    }
    $total_qty += $qty;
    $total_price += $item->price * $qty;

    // Include this item. Coupons that apply to the order subtotal affect all products.
    if ($include || $coupon->data['apply_to'] == 'subtotal') {
      $clone = clone $item;
      $clone->type = $node->type;
      $items = array_pad($items, count($items) + $qty, $clone);
    }
  }

  // If no matches were found, there are no discounts to calculate.
  if ($matched == 0) {
    return t('You do not have any applicable products in your cart.');
  }
  $use_matched = isset($coupon->data['minimum_qty_restrict']) && $coupon->data['minimum_qty_restrict'] != FALSE;

  // Make sure the minimum quantity restriction (if any) is met.
  if (isset($coupon->data['minimum_qty'])) {
    if (($use_matched ? $matched : $total_qty) < (int) $coupon->data['minimum_qty']) {
      return t('You do not have enough applicable products in your cart.');
    }
  }

  // Make sure the minimum order total restriction (if any) is met.
  if ($coupon->minimum_order > 0) {
    if (($use_matched ? $matched_price : $total_price) < $coupon->minimum_order) {
      return $use_matched ? t('You have not reached the minimum total of applicable products for this coupon.') : t('You have not reached the minimum order total for this coupon.');
    }
  }

  // Ensure that all products match, if specified.
  if (isset($coupon->data['require_match_all']) && $matched < $total_qty) {
    return t('You have non-applicable products in your cart');
  }

  // Slice off applicable products if a limit was set.
  switch ($coupon->data['apply_to']) {
    case 'cheapest':
      usort($items, '_uc_coupon_sort_products');
      $items = array_slice($items, 0, $coupon->data['apply_count']);
      break;
    case 'expensive':
      usort($items, '_uc_coupon_sort_products');
      $items = array_slice($items, -$coupon->data['apply_count']);
      break;
  }

  // Build the discounts array and get the order total.
  $total = 0;
  $discounts = array();
  $included_rates = array();
  foreach ($items as $item) {
    if (!isset($discounts[$item->nid])) {

      // First entry for this product.
      // Calculate the pre-tax discount proportion for this item.
      // For fixed discounts to products with taxes included, we apply the face value of the coupon
      // tax-inclusively also; that is, the actual discount is reduced so that the face value is
      // realized after taxes. (This already happens automatically for percentage based coupons).
      $included_rate = 1;
      if (module_exists('uc_taxes')) {
        foreach (uc_taxes_rate_load() as $tax) {
          if ($tax->display_include && is_array($tax->taxed_line_items) && in_array('coupon', $tax->taxed_line_items) && in_array($item->type, $tax->taxed_product_types) && ($tax->shippable == 0 || $item->data['shippable'] == 1)) {
            $included_rate += $tax->rate;
          }
        }
      }

      // Adjust the price for any stacked coupons.
      $prior_discount = 0;
      if (!empty($order->data['coupons'])) {
        foreach ($order->data['coupons'] as $stacked) {
          if (isset($stacked[$item->nid])) {
            $prior_discount += $stacked[$item->nid]->pretax_discount;
          }
        }
      }
      $total -= $prior_discount * $included_rate;
      $discounts[$item->nid] = (object) array(
        'qty' => 1,
        'price' => $item->price - $prior_discount,
      );
      $included_rates[$item->nid] = $included_rate;
      unset($item->type);
    }
    else {

      // An entry for this product already exists.
      // Add this item to the total for the product.
      $discounts[$item->nid]->price += $item->price;
      $discounts[$item->nid]->qty++;
    }
    $total += $item->price * $included_rate;
  }

  // Calculate the discounts per product.
  foreach ($discounts as $nid => $discount) {
    $inclusive_price = $discount->price * $included_rates[$nid];
    switch ($coupon->type) {
      case 'price':
        if ($coupon->data['apply_to'] == 'subtotal' || $coupon->data['apply_to'] == 'products_total') {

          // Apply single discount proportionally across all matching items.
          $discount->discount = $total == 0 ? 0 : min($coupon->value * ($inclusive_price / $total), $inclusive_price);
        }
        else {

          // Apply discount to each product.
          $discount->discount = min($coupon->value * $discount->qty, $inclusive_price);
        }
        break;
      case 'percentage':
        $discount->discount = $inclusive_price * $coupon->value / 100;
        break;
      case 'set_price':
        $discount->discount = max($inclusive_price - $coupon->value * $discount->qty, 0);
        break;
    }
    $discount->pretax_discount = $discount->discount / $included_rates[$nid];
    unset($discount->price);
    unset($discount->qty);
  }
  return $discounts;
}
function _uc_coupon_match_sku($model, $skus) {
  foreach ($skus as $match) {
    if (preg_match('/^' . str_replace('\\*', '.*?', preg_quote($match, '/')) . '$/', $model)) {
      return TRUE;
    }
  }
  return FALSE;
}
function _uc_coupon_sort_products($a, $b) {
  if ($a->price == $b->price) {
    return 0;
  }
  return $a->price > $b->price ? 1 : -1;
}

/**
 * Lists all taxonomy terms contained in 'taxonomy_term_reference' fields for a given node.
 * @param $node
 *   The node whose terms should be listed;
 * @return
 *   An array of any taxonomy term id's.
 */
function _uc_coupon_list_terms($node) {
  $terms = array();
  foreach (array_keys(field_info_instances('node', $node->type)) as $field_name) {
    $field_info = field_info_field($field_name);
    if ($field_info['type'] == 'taxonomy_term_reference') {
      if ($field_values = field_get_items('node', $node, $field_name)) {
        foreach ($field_values as $field_value) {
          $terms[] = $field_value['tid'];
        }
      }
    }
  }
  return $terms;
}

/**
 * Implements hook_block_info().
 */
function uc_coupon_block_info() {
  $blocks = array();
  $blocks['coupon-discount'] = array(
    'info' => t('Coupon discount form'),
  );
  return $blocks;
}

/**
 * Implements hook_block_view().
 */
function uc_coupon_block_view($delta) {
  if ($delta == 'coupon-discount') {
    $block = array(
      'subject' => t('Coupon discount'),
      'content' => drupal_get_form('uc_coupon_form', 'block'),
    );
    return $block;
  }
}

/**
 * Default theme implementation for the coupon submit form.
 */
function theme_uc_coupon_form($variables) {
  $form = $variables['form'];
  $output = '';
  if ($form['#uc_coupon_form_context'] == 'cart') {
    $output .= '<h3>' . t('Coupon discounts') . '</h3>';
  }
  elseif ($form['#uc_coupon_form_context'] == 'block') {
    if (isset($form['code'])) {
      $form['code']['#size'] = 15;
    }
  }
  $output .= drupal_render_children($form);
  return $output;
}

/**
 * Implements hook_uc_cart_pane().
 */
function uc_coupon_uc_cart_pane($items) {
  drupal_add_css(drupal_get_path('module', 'uc_coupon') . '/uc_coupon.css');

  // The coupon entry cart pane.
  $body = drupal_get_form('uc_coupon_form', 'cart') + array(
    '#prefix' => '<div id="uc-cart-pane-coupon">',
    '#suffix' => '</div>',
  );
  $panes[] = array(
    'id' => 'coupon',
    'body' => $body,
    'title' => t('Coupon discount'),
    'desc' => t('Allows shoppers to use a coupon during checkout for order discounts.'),
    'weight' => 1,
    'enabled' => TRUE,
  );

  // The "Special Discounts" cart pane.
  $body = array();
  $discounts = _uc_coupon_options_list(uc_coupon_session_validate(), FALSE);
  if (!empty($discounts)) {
    $body = array(
      '#theme' => 'uc_coupon_automatic_discounts',
      '#prefix' => '<div id="uc-cart-pane-coupon-automatic">',
      '#title' => t('Special discounts'),
      '#suffix' => '</div>',
      'discounts' => array(
        '#theme' => 'item_list',
        '#items' => _uc_coupon_options_list(uc_coupon_session_validate(), FALSE),
        '#title' => t('Special discounts'),
      ),
    );
  }
  $panes[] = array(
    'id' => 'coupon_auto',
    'body' => $body,
    'title' => t('Special Discounts'),
    'desc' => t('Displays a list of automatic discounts.'),
    'weight' => 1,
    'enabled' => TRUE,
  );
  return $panes;
}
function theme_uc_coupon_automatic_discounts($variables) {
  $form = $variables['form'];

  /*
  $items = $form['discounts']['#items'];
  $title = isset($form['discounts']['#title']) ? $form['discounts']['#title'] : NULL;
  $rows = array();
  foreach($items as $item) {
    $rows[] = array($item);
  }
  $form['discounts'] = array(
    '#theme' => 'table',
    '#rows' => $rows,
  );
  if (isset($title)) {
    $form['discounts']['#header'] = array($title);
  }
  */
  return drupal_render_children($form);
}

/**
 * Create a tapir table of validated coupons with a "Remove" button for each.
 *
 * @param $coupons
 *     An array of coupon options of the form code => title
 * @param $ajax
 *     The #ajax array to apply to each remove button, or FALSE if the form
 *    is not ajax enabled.
 */
function uc_coupon_table($coupons, $ajax = FALSE) {
  $table = array(
    '#type' => 'tapir_table',
  );
  $table['#columns'] = array(
    'title' => array(
      'cell' => t('Active coupons'),
      'weight' => 0,
    ),
    'remove' => array(
      'cell' => t('Remove'),
      'weight' => 1,
    ),
  );
  $i = 0;
  foreach ($coupons as $code => $title) {
    $table[$i] = array(
      'title' => array(
        '#markup' => $title,
      ),
      'remove' => array(
        '#type' => 'submit',
        '#value' => t('Remove'),
        '#name' => 'uc-coupon-remove-' . $code,
      ),
    );
    if ($ajax) {

      // Add ajax functionality to this table.
      $table[$i]['remove'] += array(
        '#ajax' => $ajax,
      );
    }
    $i++;
  }
  return $table;
}

/**
 * Helper function to create a list of coupons for form elements.
 * @param $coupons
 *     An array of validated coupon objects to include in the list.
 * @param $in_session
 *     TRUE to include only coupons currently added to the session.
 *     FALSE to include those not in the session (e.g. automatic coupons).
 * @return
 *     An associative array mapping coupon code to coupon title.
 */
function _uc_coupon_options_list($coupons, $in_session = TRUE) {
  $options = array();
  if (!empty($coupons)) {
    foreach ($coupons as $coupon) {
      if (!$in_session xor uc_coupon_session_get($coupon->code)) {
        $options[$coupon->code] = $coupon->title;
      }
    }
  }
  return $options;
}
function uc_coupon_checkout_submit($form, &$form_state) {
  unset($form_state['checkout_valid']);
  $form_state['redirect'] = 'cart/checkout';
  $form_state['rebuild'] = TRUE;
}

/**
 * Form builder for the uc_coupon form.
 *
 * @param $context
 *     Where the form is to appear: 'cart', 'block' or 'checkout'
 * @param $ajax
 *     The #ajax array to be applied to all buttons, or FALSE for a non-ajax-enabled form.
 */
function uc_coupon_form($form, $form_state, $context = 'block', $ajax = FALSE) {
  $coupons = uc_coupon_session_validate();
  $components = variable_get('uc_coupon_form_components', drupal_map_assoc(variable_get('uc_coupon_allow_multiple', FALSE) ? array(
    'entry',
  ) : array(
    'entry',
    'list',
  )));

  // Show the coupon code entry component
  if (!empty($components['entry'])) {
    $form['code'] = array(
      '#type' => 'textfield',
      '#size' => 25,
      '#title' => t('Coupon Code'),
      '#description' => t('Enter a coupon code and click "Apply to order" below.'),
    );
    $form['apply'] = array(
      '#type' => 'submit',
      '#value' => t('Apply to order'),
      '#name' => 'uc-coupon-apply',
    );
    if (!variable_get('uc_coupon_allow_multiple', FALSE) && count(uc_coupon_session_get()) > 0) {
      $form['code']['#description'] .= ' ' . t('Apply a blank code to remove the currently applied coupon.');
    }
    if ($ajax) {

      // Ensure that the ajax throbber doesn't spoil the layout.
      drupal_add_css('#uc-coupon-active-coupons, #uc-coupon-other-discounts { clear: left; }', array(
        'type' => 'inline',
        'group' => CSS_DEFAULT,
      ));
      $form['apply'] += array(
        '#ajax' => $ajax,
      );
    }
    if ($context == 'checkout') {
      $form['apply']['#limit_validation_errors'] = array();
      $form['apply']['#submit'] = array(
        'uc_coupon_checkout_submit',
      );
    }
  }

  // Add active coupons components (table and/or list).
  $options = _uc_coupon_options_list($coupons);
  if (!empty($options)) {
    if (!empty($components['table'])) {
      $form['coupons_table'] = tapir_get_table('uc_coupon_table', $options, $ajax);
    }
    if (!empty($components['list'])) {
      $form['coupons'] = array(
        '#prefix' => '<div id="uc-coupon-active-coupons">',
        '#suffix' => '</div>',
        '#type' => 'checkboxes',
        '#title' => t('Active Coupons'),
        '#options' => $options,
        '#default_value' => array_keys($options),
        '#description' => t('These coupons have been applied to your order. To remove one, uncheck the box and click "Remove coupons" below.'),
      );
      $form['coupons']['#description'] = t('These coupons have been applied to your order. To remove one, uncheck the box next to the coupon name and click "Update order" below.');
      $form['remove'] = array(
        '#type' => 'submit',
        '#value' => t('Update order'),
        '#name' => 'uc-coupon-remove',
      );
      if ($ajax) {
        $form['remove']['#ajax'] = $ajax;
      }
      if ($context == 'checkout') {
        $form['remove']['#limit_validation_errors'] = array();
        $form['remove']['#submit'] = array(
          'uc_coupon_checkout_submit',
        );
      }
    }
  }

  // Add context to help out themers.
  $form['#uc_coupon_form_context'] = $context;
  return $form;
}

/**
 * Implements hook_uc_order().
 *
 * This makes sure the active, validated coupons are saved to the order.
 * It also prevents any fake coupon cart-items from being saved to the order.
 */
function uc_coupon_uc_order($op, &$order) {
  if ($op == 'presave') {

    // Apply any session coupons to the current cart order.
    if (_uc_coupon_is_checkout_order($order)) {
      $coupons = uc_coupon_session_validate();

      // Index existing line items by coupon code.
      $items = array();
      foreach ($order->line_items as $index => $line) {
        if ($line['type'] == 'coupon') {

          // For orders created before multi-coupons were enabled, the code was not saved with the line item.
          // In this case, we retreive it from the uc_coupons_orders table.
          $code = isset($line['data']['code']) ? $line['data']['code'] : db_query('SELECT code FROM {uc_coupons_orders} WHERE oid = :oid', array(
            ':oid' => $order->order_id,
          ))
            ->fetchField();
          $items[$code] = $index;
        }
      }

      // Index existing entries in {uc_coupons_orders} by coupon code.
      $entries = db_query('SELECT code, cuid FROM {uc_coupons_orders} WHERE oid = :oid', array(
        ':oid' => $order->order_id,
      ))
        ->fetchAllKeyed(0, 1);

      // Update, insert or delete line items and entries in uc_coupons_orders.
      $insert = array();
      $order->data['coupons'] = array();
      foreach ($coupons as $coupon) {
        $order->data['coupons'][$coupon->code] = $coupon->discounts;

        // Handle entries in {uc_coupons_orders}.
        if (isset($entries[$coupon->code])) {
          db_update('uc_coupons_orders')
            ->condition('cuid', $entries[$coupon->code])
            ->fields(array(
            'cid' => $coupon->cid,
            'value' => $coupon->amount,
          ))
            ->execute();
          unset($entries[$coupon->code]);
        }
        else {
          $insert[] = array(
            $coupon->cid,
            $order->order_id,
            $coupon->code,
            $coupon->value,
          );
        }

        // Handle line items.
        if (isset($items[$coupon->code])) {
          $line =& $order->line_items[$items[$coupon->code]];
          $line['title'] = $coupon->title;
          $line['amount'] = -$coupon->pretax_amount;
          $line['data']['code'] = $coupon->code;
          uc_order_update_line_item($line['line_item_id'], $line['title'], $line['amount'], $line['data']);
          unset($items[$coupon->code]);
        }
        else {

          // Create a new line item.
          $order->line_items[] = uc_order_line_item_add($order->order_id, 'coupon', $coupon->title, -$coupon->pretax_amount, _uc_line_item_data('coupon', 'weight'), array(
            'code' => $coupon->code,
          ));
        }
      }

      // Insert new entries in {uc_coupons_orders}
      if (!empty($insert)) {
        $query = db_insert('uc_coupons_orders');
        $query
          ->fields(array(
          'cid',
          'oid',
          'code',
          'value',
        ));
        foreach ($insert as $fields) {
          $query
            ->values($fields);
        }
        $query
          ->execute();
      }

      // Delete orphaned entries in {uc_coupons_orders}
      if (!empty($entries)) {
        db_delete('uc_coupons_orders')
          ->condition('cuid', $entries)
          ->execute();
      }

      // Remove orphaned line-items.
      foreach ($items as $index) {
        uc_order_delete_line_item($order->line_items[$index]['line_item_id']);
        unset($order->line_items[$index]);
      }
      usort($order->line_items, 'uc_weight_sort');
    }

    // Make sure any fake cart items don't get saved with the order if the checkout page is skipped
    // (e.g. Paypal Express Checkout, Google Checkout)
    foreach ($order->products as $key => $product) {
      if (isset($product->module) && $product->module == 'uc_coupon') {
        unset($order->products[$key]);
      }
    }
  }
}

/**
 * Implements hook_uc_checkout_pane().
 *
 * Show a pane just above the order total that allows shoppers to enter a coupon
 * for a discount.
 */
function uc_coupon_uc_checkout_pane() {
  $panes[] = array(
    'id' => 'coupon',
    'callback' => 'uc_checkout_pane_coupon',
    'title' => t('Coupon discount'),
    'desc' => t('Allows shoppers to use a coupon during checkout for order discounts.'),
    'weight' => 5,
    'process' => TRUE,
  );
  $panes[] = array(
    'id' => 'coupon_automatic',
    'callback' => 'uc_checkout_pane_coupon_automatic',
    'title' => t('Special Discounts'),
    'desc' => t('Displays a list of all automatic coupon discounts.'),
    'weight' => 5,
    'process' => FALSE,
  );
  return $panes;
}

/**
 * Ajax callback for checkout form.
 */
function uc_coupon_checkout_update($form, $form_state) {
  $commands[] = ajax_command_replace('#coupon-pane', trim(drupal_render($form['panes']['coupon'])));
  if (isset($form['panes']['coupon_automatic'])) {
    $commands[] = ajax_command_replace('#coupon_automatic-pane', trim(drupal_render($form['panes']['coupon_automatic'])));
  }
  if (isset($form['panes']['payment'])) {
    $commands[] = ajax_command_replace('#payment-pane', trim(drupal_render($form['panes']['payment'])));
  }
  if (isset($form['panes']['quotes'])) {
    $commands[] = ajax_command_replace('#quotes-pane', trim(drupal_render($form['panes']['quotes'])));
  }
  if (variable_get('uc_coupon_show_in_cart', TRUE) && isset($form['panes']['cart']['cart_review_table'])) {
    $commands[] = ajax_command_html('#cart-pane>div', drupal_render($form['panes']['cart']['cart_review_table']));
  }

  // Clear the coupon code, but only if the submission was successful.
  if (count(drupal_get_messages('error', FALSE)) == 0) {
    $commands[] = ajax_command_invoke('#coupon-pane input[type=text]', 'val', array(
      '',
    ));
  }

  // Make sure all checkboxes are checked.
  $commands[] = ajax_command_invoke('#coupon-pane input[type=checkbox]', 'attr', array(
    'checked',
    'true',
  ));

  // Show any messages.
  $commands[] = ajax_command_html('#coupon-messages', theme('status_messages'));
  return array(
    '#type' => 'ajax',
    '#commands' => $commands,
  );
}

/**
 * A checkout pane listing any automatic discounts.
 */
function uc_checkout_pane_coupon_automatic($op, &$order, $form = NULL, &$form_state = NULL) {
  if ($op == 'view') {
    $discounts = _uc_coupon_options_list(uc_coupon_session_validate(), FALSE);
    if (empty($discounts)) {
      $inner_contents = array(
        '#markup' => t('None.'),
      );
    }
    else {
      $inner_contents = array(
        '#theme' => 'item_list',
        '#items' => _uc_coupon_options_list(uc_coupon_session_validate(), FALSE),
      );
    }
    return array(
      'theme' => 'uc_coupon_automatic_discounts',
      'contents' => array(
        'discounts' => $inner_contents,
      ),
    );
  }
}

/**
 * Checkout Pane callback function.
 *
 * Used to display a form in the checkout process so that customers
 * can enter discount coupons.
 */
function uc_checkout_pane_coupon($op, &$order, $form = NULL, &$form_state = NULL) {
  switch ($op) {
    case 'prepare':

      // Remove fake cart items from the order.
      foreach ($order->products as $key => $product) {
        if (isset($product->module) && $product->module == 'uc_coupon') {
          unset($order->products[$key]);
        }
      }
      break;
    case 'view':
      drupal_add_css('#coupon-messages { clear: both; }', array(
        'type' => 'inline',
        'group' => CSS_DEFAULT,
      ));
      $ajax = array(
        'callback' => 'uc_coupon_checkout_update',
      );
      $description = variable_get('uc_coupon_pane_description', t('Enter a coupon code for this order.'));
      $contents = uc_coupon_form(array(), $form_state, 'checkout', $ajax);
      $contents['message'] = array(
        '#markup' => '<div id="coupon-messages"></div>',
        '#weight' => 2,
      );
      return array(
        'description' => $description,
        'contents' => $contents,
        'theme' => 'uc_coupon_form',
      );
    case 'process':
      $trigger = $form_state['triggering_element']['#name'];
      if (substr($trigger, 0, 9) == 'uc-coupon') {
        $form_state['rebuild'] = TRUE;
        uc_coupon_form_submit($form, $form_state);
        uc_coupon_session_validate($order->products);
        return FALSE;

        // Prevent redirection.
      }
      else {

        // !TODO Coupon will not be validated if "Apply to order" is not clicked.  Is this what we want?
        return TRUE;
      }
    case 'settings':
      $form['uc_coupon_collapse_pane'] = array(
        '#type' => 'checkbox',
        '#title' => t('Collapse checkout pane by default.'),
        '#default_value' => variable_get('uc_coupon_collapse_pane', FALSE),
      );
      $form['uc_coupon_pane_description'] = array(
        '#type' => 'textarea',
        '#title' => t('Checkout pane message'),
        '#default_value' => variable_get('uc_coupon_pane_description', t('Enter a coupon code for this order.')),
      );
      return $form;
  }
}

/**
 * Submit handler for the uc_coupon form.
 */
function uc_coupon_form_submit($form, $form_state) {
  $trigger = $form_state['triggering_element']['#name'];

  // Determine where the values are (they will be in the pane subarray if called from checkout page).
  $values = isset($form_state['values']['panes']['coupon']) ? $form_state['values']['panes']['coupon'] : $form_state['values'];

  // If this was the result of a 'remove' submission.
  if (substr($trigger, 0, 16) == 'uc-coupon-remove') {

    // See if there was an individual remove button clicked.
    $code = substr($trigger, 17);
    if (!empty($code)) {
      uc_coupon_session_clear($code);
      drupal_set_message(t('Coupon "%code" has been removed from your order', array(
        '%code' => $code,
      )));
      module_invoke_all('uc_coupon_remove', uc_coupon_find($code));
    }
    elseif (isset($values['coupons'])) {
      $removed = array();
      foreach ($values['coupons'] as $code => $selected) {
        if (!$selected) {
          $removed[] = $code;
          uc_coupon_session_clear($code);
          module_invoke_all('uc_coupon_remove', uc_coupon_find($code));
        }
      }
      $n = count($removed);
      if ($n > 1) {
        $last = $removed[$n - 1];
        $rest = implode(', ', array_slice($removed, 0, $n - 1));
        drupal_set_message(t('Coupons %rest and %last have been removed from your order.', array(
          '%rest' => $rest,
          '%last' => $last,
        )));
      }
      elseif (!empty($removed)) {
        drupal_set_message(t('Coupon %code has been removed from your order', array(
          '%code' => $removed[0],
        )));
      }
    }
  }
  else {
    $code = strtoupper(trim($values['code']));
    $removed = FALSE;

    // If multiple codes are not enabled, then remove any codes currently applied.
    if (!variable_get('uc_coupon_allow_multiple', FALSE) && count($session = uc_coupon_session_get()) > 0) {
      foreach (array_keys($session) as $remove_code) {
        uc_coupon_session_clear($remove_code);
        drupal_set_message(t('Coupon "%code" has been removed from your order', array(
          '%code' => $remove_code,
        )));
        module_invoke_all('uc_coupon_remove', uc_coupon_find($remove_code));
      }
      $removed = TRUE;
    }
    if (!empty($code)) {
      uc_coupon_session_add($code, 'submit');
    }
    elseif (!$removed) {
      drupal_set_message(t("You must enter a valid coupon code."), 'error');
    }
  }
}

/**
 * Implements hook_uc_line_item().
 */
function uc_coupon_uc_line_item() {
  $items[] = array(
    'id' => 'coupon',
    'title' => t('Coupon discount'),
    'tax_adjustment' => 'uc_coupon_tax_adjustment',
    'weight' => 0,
    'default' => FALSE,
    'stored' => TRUE,
    'add_list' => TRUE,
    'calculated' => TRUE,
  );
  return $items;
}

/**
 * Handle tax on coupons by calculating tax for individual discounted prices.
 * This is designed to work with a patch to uc_taxes from http://drupal.org/node/1155656.
 */
function uc_coupon_tax_adjustment($price, $order, $tax) {
  $amount = 0;
  if (isset($order->data['coupons'])) {
    foreach ($order->data['coupons'] as $discounts) {
      foreach ($discounts as $nid => $item) {
        $node = node_load($nid);
        if (in_array($node->type, $tax->taxed_product_types) && ($tax->shippable == 0 || $node->shippable == 1)) {
          $amount += (isset($item->pretax_discount) ? $item->pretax_discount : $item->discount) * ($price > 0 ? 1 : -1);
        }
      }
    }
  }
  return $amount;
}

/**
 * Show a message if PayPal is enabled and "itemized order" is selected.
 */
function _uc_coupon_paypal_check() {
  if (variable_get('uc_payment_method_paypal_wps_checkout', 0) && variable_get('uc_paypal_wps_submit_method', 'single') == 'itemized') {
    drupal_set_message(t('To use coupons with PayPal you must select "Submit the whole order as a single line item". <a href="!url">Click here to change this setting</a>.', array(
      '!url' => url('admin/store/settings/payment/edit/methods'),
    )));
  }
}

/**
 * Implements hook_uc_store_status().
 */
function uc_coupon_uc_store_status() {
  $statuses = array();
  if (variable_get('uc_payment_method_paypal_wps_checkout', 0) && variable_get('uc_paypal_wps_submit_method', 'single') == 'itemized') {
    $statuses[] = array(
      'status' => 'warning',
      'title' => t('Coupons'),
      'desc' => t('To use coupons with PayPal you must select "Submit the whole order as a single line item". <a href="!url">Click here to change this setting</a>.', array(
        '!url' => url('admin/store/settings/payment/edit/methods'),
      )),
    );
  }
  return $statuses;
}

/**
 * Implements hook_uc_cart_alter().
 *
 * This is called every time the cart is rebuild (e.g. when products are added), so it's a good place
 * to revalidate our session coupons.  We also add a fake cart item (if configured to show in cart)
 * for each coupon.  These will be removed at checkout.
 */
function uc_coupon_uc_cart_alter(&$items) {
  $coupons = uc_coupon_session_validate($items);

  // Validate all codes in the session against the cart contents.
  if (variable_get('uc_coupon_show_in_cart', TRUE) && !empty($coupons)) {

    // If there are some valid coupons, then add them to the cart.
    foreach ($coupons as $code => $coupon) {
      $items[] = _uc_coupon_cart_item($coupon);
    }
  }
}

/**
 * Creates a fake cart-item corrresponding to this coupon, allowing this coupon to be displayed in the cart.
 *
 * @param $coupon
 *   The coupon to be displayed in the cart.
 */
function _uc_coupon_cart_item($coupon) {

  // Assign this a unique cart_item_id so it will be keyed properly by entity_view().
  $id = -hexdec(substr(sha1($coupon->code), -8));
  return (object) array(
    'cart_item_id' => $id,
    'module' => 'uc_coupon',
    'title' => $coupon->title,
    'nid' => 0,
    'qty' => 1,
    'price' => -$coupon->amount,
    'data' => array(
      'module' => 'uc_coupon',
      'shippable' => FALSE,
      'code' => $coupon->code,
      'remove' => uc_coupon_session_get($coupon->code),
    ),
    'model' => 0,
    'weight' => 0,
  );
}

/**
 * Implements hook_uc_cart_display().
 */
function uc_coupon_uc_cart_display($item) {
  $display_item = array(
    'module' => array(
      '#type' => 'value',
      '#value' => 'uc_coupon',
    ),
    'nid' => array(
      '#type' => 'value',
      '#value' => 0,
    ),
    'title' => array(
      '#markup' => $item->title,
    ),
    'description' => array(
      '#markup' => '',
    ),
    'qty' => array(
      '#type' => 'hidden',
      '#value' => 1,
      '#default_value' => 1,
    ),
    '#total' => $item->price,
    'data' => array(
      '#type' => 'hidden',
      '#value' => serialize($item->data),
    ),
    '#suffixes' => array(),
  );
  if ($item->data['remove']) {
    $display_item['remove'] = array(
      '#type' => 'submit',
      '#value' => t('Remove'),
    );
  }
  return $display_item;
}

/**
 * Implements hook_uc_update_cart_item().
 * Remove a coupon from the order when the "Remove" button is clicked.
 */
function uc_coupon_uc_update_cart_item($nid, $data, $qty) {
  if (isset($data['code']) && $qty == 0) {
    uc_coupon_session_clear($data['code']);
    module_invoke_all('uc_coupon_remove', uc_coupon_find($data['code']));
  }
}

/**
 * Theme override for the default cart block content.
 * Removes coupons from the total number of items.
 */
function uc_coupon_theme_uc_cart_block_content($variables) {
  if (variable_get('uc_coupon_show_in_cart', TRUE) && !empty($variables['items'])) {
    foreach ($variables['items'] as &$item) {
      if ($item['nid'] == 0 && $item['price'] <= 0) {
        $item['qty'] = '';
        $variables['item_count']--;
      }
    }
    $variables['item_text'] = format_plural($variables['item_count'], '<span class="num-items">1</span> Item', '<span class="num-items">@count</span> Items');
  }
  return theme_uc_cart_block_content($variables);
}

/**
 * Implements hook_form_FORM_ID_alter() for uc_cart_checkout_form().
 *
 * Remove any coupon cart items from the serialized cart contents and payment-pane
 * order, as coupons will be handled as line items during checkout.
 *
 * Collapse coupon checkout pane, if configured to do so.
 */
function uc_coupon_form_uc_cart_checkout_form_alter(&$form, $form_state) {
  if (variable_get('uc_coupon_collapse_pane', FALSE) && isset($form['panes']['coupon'])) {
    $form['panes']['coupon']['#collapsed'] = TRUE;
  }

  // Show current session coupons in the cart pane (since now they will have been removed from the order).
  if (variable_get('uc_coupon_show_in_cart', TRUE) && isset($form['panes']['cart'])) {
    $coupons = uc_coupon_session_validate();

    // If there are some valid coupons, then add them to the cart.
    foreach ($coupons as $code => $coupon) {
      $item = _uc_coupon_cart_item($coupon);
      $item->order_product_id = $item->cart_item_id;
      $form['panes']['cart']['cart_review_table']['#items'][] = $item;
    }
  }
}

/**
 * Implements hook_uc_checkout_complete().
 *
 * Ensure the stored coupon code is reset after checkout.
 */
function uc_coupon_uc_checkout_complete($order, $account) {
  uc_coupon_session_clear();
}

/**
 * Preprocess template for a printed coupon certificate.
 * @see uc_coupon-certificate.tpl.php
 */
function template_preprocess_uc_coupon_certificate(&$variables) {
  $coupon = $variables['coupon'];
  $variables['value'] = theme('uc_coupon_discount', array(
    'coupon' => $coupon,
  ));
  $variables['display_name'] = check_plain($coupon->name);
  $n = stripos($variables['display_name'], 'purchased by');
  if ($n) {
    $variables['display_name'] = substr($variables['display_name'], 0, $n - 1);
  }
  if ($coupon->valid_until) {
    $variables['not_yet_valid'] = $coupon->valid_from > REQUEST_TIME;
    $variables['valid_from'] = format_date($coupon->valid_from, 'custom', variable_get('date_format_uc_store', 'm/d/Y'));
    $variables['valid_until'] = format_date($coupon->valid_until, 'custom', variable_get('date_format_uc_store', 'm/d/Y'));
  }
  else {
    $variables['not_yet_valid'] = FALSE;
    $variables['valid_from'] = FALSE;
    $variables['valid_until'] = FALSE;
  }
  $variables['max_uses_per_user'] = isset($coupon->data['max_uses_per_user']) ? $coupon->data['max_uses_per_user'] : NULL;
  $variables['include'] = array();
  $variables['exclude'] = array();
  if (isset($coupon->data['product_types'])) {
    foreach ($coupon->data['product_types'] as $type) {
      $variables['include'][] = node_type_get_name($type);
    }
  }
  if (isset($coupon->data['products'])) {
    $key = isset($coupon->data['negate_products']) ? 'exclude' : 'include';
    foreach ($coupon->data['products'] as $nid) {
      $node = node_load($nid);
      $variables[$key][] = $node->title;
    }
  }
  if (isset($coupon->data['skus'])) {
    foreach ($coupon->data['skus'] as $sku) {
      $variables['include'][] = t('SKU') . ' ' . $sku;
    }
  }
  if (isset($coupon->data['terms'])) {
    $key = isset($coupon->data['negate_terms']) ? 'exclude' : 'include';
    foreach ($coupon->data['terms'] as $tid) {
      $term = taxonomy_term_load($tid);
      $variables[$key][] = $term->name;
    }
  }

  // Merge in global tokens.
  $info = token_info();

  //dpm($info);
  foreach ($info['types'] as $type => $type_info) {
    if (empty($type_info['needs-data']) && $type != 'current-user' && $type != 'current-date') {
      $type_key = !empty($type_info['type']) ? $type_info['type'] : $type;
      if (!empty($info['tokens'][$type_key])) {
        foreach (array_keys($info['tokens'][$type_key]) as $token) {
          $variables[str_replace('-', '_', $type_key) . '_' . str_replace('-', '_', $token)] = token_replace("[{$type_key}:{$token}]");
        }
      }
    }
  }
  if (isset($variables['coupon']->data['base_cid'])) {
    $variables['theme_hook_suggestions'][] = 'uc_coupon_certificate__base_' . $variables['coupon']->data['base_cid'];
  }
  $variables['theme_hook_suggestions'][] = 'uc_coupon_certificate__' . $variables['coupon']->cid;
}

/**
 * Page template for printed coupons.
 * @see uc_coupon-page.tpl.php
 */
function template_preprocess_uc_coupon_page(&$variables) {
  $variables['styles'] = drupal_get_css();
}

/**
 * Implements hook_uc_coupon_actions().
 */
function uc_coupon_uc_coupon_actions($coupon) {
  $actions = array();
  if (user_access('view store coupons')) {
    $actions[] = array(
      'url' => 'admin/store/coupons/' . $coupon->cid,
      'icon' => drupal_get_path('module', 'uc_store') . '/images/order_view.gif',
      'title' => t('View coupon: @name', array(
        '@name' => $coupon->name,
      )),
    );
    $actions[] = array(
      'url' => 'admin/store/coupons/' . $coupon->cid . '/print',
      'icon' => drupal_get_path('module', 'uc_store') . '/images/print.gif',
      'title' => t('Print coupon: @name', array(
        '@name' => $coupon->name,
      )),
    );
    if ($coupon->bulk) {
      $actions[] = array(
        'url' => 'admin/store/coupons/' . $coupon->cid . '/codes',
        'icon' => drupal_get_path('module', 'uc_store') . '/images/menu_reports_small.gif',
        'title' => t('Download codes as CSV: @name', array(
          '@name' => $coupon->name,
        )),
      );
    }
  }
  if (user_access('manage store coupons')) {
    $actions[] = array(
      'url' => 'admin/store/coupons/' . $coupon->cid . '/edit',
      'icon' => drupal_get_path('module', 'uc_store') . '/images/order_edit.gif',
      'title' => t('Edit coupon: @name', array(
        '@name' => $coupon->name,
      )),
    );
    $actions[] = array(
      'url' => 'admin/store/coupons/' . $coupon->cid . '/delete',
      'icon' => drupal_get_path('module', 'uc_store') . '/images/order_delete.gif',
      'title' => t('Delete coupon: @name', array(
        '@name' => $coupon->name,
      )),
    );
  }
  return $actions;
}

/**
 * Implements hook_views_api().
 */
function uc_coupon_views_api() {
  return array(
    'api' => '2.0',
    'path' => drupal_get_path('module', 'uc_coupon') . '/views',
  );
}

/**
 * Check whether an order is the order being checked out by the current user.
 * @param $order
 */
function _uc_coupon_is_checkout_order($order) {
  global $user;
  return isset($_SESSION['cart_order']) && isset($order->order_id) && $order->order_id == $_SESSION['cart_order'] && uc_order_status_data($order->order_status, 'state') == 'in_checkout' && $user->uid == $order->uid;
}

Functions

Namesort descending Description
template_preprocess_uc_coupon_certificate Preprocess template for a printed coupon certificate.
template_preprocess_uc_coupon_page Page template for printed coupons.
theme_uc_coupon_automatic_discounts
theme_uc_coupon_discount Theme for a coupon discount.
theme_uc_coupon_form Default theme implementation for the coupon submit form.
uc_checkout_pane_coupon Checkout Pane callback function.
uc_checkout_pane_coupon_automatic A checkout pane listing any automatic discounts.
uc_coupon_block_info Implements hook_block_info().
uc_coupon_block_view Implements hook_block_view().
uc_coupon_calculate_discounts Find items that a coupon will apply to and calculate the discounts.
uc_coupon_checkout_submit
uc_coupon_checkout_update Ajax callback for checkout form.
uc_coupon_count_usage Count usage of a coupon.
uc_coupon_find Load a coupon (single or bulk) from the supplied code.
uc_coupon_form Form builder for the uc_coupon form.
uc_coupon_form_submit Submit handler for the uc_coupon form.
uc_coupon_form_uc_cart_checkout_form_alter Implements hook_form_FORM_ID_alter() for uc_cart_checkout_form().
uc_coupon_get_bulk_code Generate a single bulk coupon code.
uc_coupon_init Implements hook_init().
uc_coupon_menu Implements hook_menu().
uc_coupon_permission Implements hook_permission().
uc_coupon_session_add Adds or updates a coupon code for the current session.
uc_coupon_session_clear Removes one (or all) coupon codes from the session.
uc_coupon_session_get Checks to see if a given code is present in the session, or returns an associative array of all codes in the session.
uc_coupon_session_validate Validates all coupons in the current session. The validated coupons are statically cached for each request. The cache is rebuilt the first time this function is called, or every time the cart contents are rebuilt.
uc_coupon_table Create a tapir table of validated coupons with a "Remove" button for each.
uc_coupon_tax_adjustment Handle tax on coupons by calculating tax for individual discounted prices. This is designed to work with a patch to uc_taxes from http://drupal.org/node/1155656.
uc_coupon_theme Implements hook_theme().
uc_coupon_theme_registry_alter Implements hook_theme_registry_alter().
uc_coupon_theme_uc_cart_block_content Theme override for the default cart block content. Removes coupons from the total number of items.
uc_coupon_title Title callback for coupon print preview.
uc_coupon_to_arg Properly handle %uc_coupon wildcard. (Necessary to prevent PHP runtime notice.)
uc_coupon_uc_cart_alter Implements hook_uc_cart_alter().
uc_coupon_uc_cart_display Implements hook_uc_cart_display().
uc_coupon_uc_cart_pane Implements hook_uc_cart_pane().
uc_coupon_uc_checkout_complete Implements hook_uc_checkout_complete().
uc_coupon_uc_checkout_pane Implements hook_uc_checkout_pane().
uc_coupon_uc_coupon_actions Implements hook_uc_coupon_actions().
uc_coupon_uc_coupon_validate Implements hook_uc_coupon_validate().
uc_coupon_uc_line_item Implements hook_uc_line_item().
uc_coupon_uc_order Implements hook_uc_order().
uc_coupon_uc_store_status Implements hook_uc_store_status().
uc_coupon_uc_update_cart_item Implements hook_uc_update_cart_item(). Remove a coupon from the order when the "Remove" button is clicked.
uc_coupon_validate Validate a coupon, and optionally calculate the order discount.
uc_coupon_views_api Implements hook_views_api().
_uc_coupon_cart_item Creates a fake cart-item corrresponding to this coupon, allowing this coupon to be displayed in the cart.
_uc_coupon_format_discount Format a coupon's value depending on the type, optionally including currency symbols.
_uc_coupon_is_checkout_order Check whether an order is the order being checked out by the current user.
_uc_coupon_list_terms Lists all taxonomy terms contained in 'taxonomy_term_reference' fields for a given node.
_uc_coupon_match_sku
_uc_coupon_options_list Helper function to create a list of coupons for form elements.
_uc_coupon_paypal_check Show a message if PayPal is enabled and "itemized order" is selected.
_uc_coupon_sort_products