You are here

uc_coupon.module in Ubercart Discount Coupons 7.3

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

Provides discount codes and gift certificates for Ubercart.

Version: 2.x Drupal Core: 7.x Ubercart Core: 3.x

Original code by Blake Lucchesi (www.boldsource.com)

Maintained by Chris Oden (wodenx@gmail.com) David Long (dave@longwaveconsulting.com)

Please submit issues, questions or feedback to the issue queue at http://drupal.org/project/uc_coupon

File

uc_coupon.module
View source
<?php

/**
 * @file
 * Provides discount codes and gift certificates for Ubercart.
 *
 * Version: 2.x
 * Drupal Core: 7.x
 * Ubercart Core: 3.x
 *
 * Original code by Blake Lucchesi (www.boldsource.com)
 *
 * Maintained by
 *   Chris Oden (wodenx@gmail.com)
 *   David Long (dave@longwaveconsulting.com)
 *
 * Please submit issues, questions or feedback to the issue queue at
 * http://drupal.org/project/uc_coupon
 */

/**
 * 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/settings/coupon/settings'] = array(
    'title' => 'Settings',
    'description' => 'Edit the basic coupon settings.',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $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.
 * @param array $exclude_oids
 *   (optional) If supplied, will exclude usage for the specified order ids.
 *
 * @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, $exclude_oids = array()) {
  global $user;
  $weight = uc_order_status_data(variable_get('uc_coupon_used_order_status', 'processing'), 'weight');
  $usage = array(
    'codes' => array(),
    'value' => array(
      'codes' => array(),
    ),
  );
  $exclude_where = empty($exclude_oids) ? '' : 'AND uo.order_id NOT IN (:oids)';
  $result = db_query("SELECT uco.code, COUNT(*) AS uses, SUM(uco.value) AS value 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 {$exclude_where} GROUP BY uco.code", array(
    ':weight' => $weight,
    ':cid' => $cid,
    ':oids' => $exclude_oids,
  ));
  foreach ($result as $row) {
    $usage['codes'][$row->code] = $row->uses;
    $usage['value']['codes'][$row->code] = $row->value;
  }
  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':
    case 'credit':
      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, TRUE);
      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($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 $order
 *   An order against which to validate the currently applied codes. If specified
 *   the cached list of valid coupons is rebuilt by revalidating all the codes in
 *   the session against that order.
 *
 * @return
 *     An array of fully validated coupon objects, indexed by code.
 */
function uc_coupon_session_validate($order = NULL) {
  static $valids = NULL;

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

    // We don't want to modify the order passed in.
    // Allow modules an opportunity to add or remove coupons from the session.
    module_invoke_all('uc_coupon_revalidate', $order);

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

      // Process all coupons in the session.
      global $user;
      $order->data['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;
}

/**
 * Validates a list of coupon codes against a specified order and account.
 *
 * @param $codes
 *   The codes to be validated.
 * @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.
 *
 * @see uc_coupon_validate()
 * @see uc_coupon_session_validate()
 */
function uc_coupon_validate_multiple($codes, $order, $account) {
  $order = clone $order;

  // We don't want to modify the order passed in.
  $order->data['coupons'] = array();
  $valids = array();
  $invalids = array();
  foreach ($codes as $code) {
    $coupon = uc_coupon_validate($code, $order, $account);
    if ($coupon->valid) {

      // Process valid coupons.
      $valids[$code] = $coupon;
      $order->data['coupons'][$code] = $coupon->discounts;
    }
    else {
      $invalids[$code] = $code;
    }
  }
  return array(
    'valid' => $valids,
    'invalid' => $invalids,
  );
}

/**
 * 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;
  }

  // Count usage for this coupon.
  $uid = !empty($account) ? $account->uid : NULL;

  // If the order exists, don't count it towards coupon usage.
  $oids = !empty($order->order_id) ? array(
    $order->order_id,
  ) : array();
  $coupon->usage = uc_coupon_count_usage($coupon->cid, $uid, $oids);

  // Calculate the discounts (if any).
  uc_coupon_prepare($coupon, $code, uc_coupon_calculate_discounts($coupon, $order));

  // 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 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;
}

/**
 * Prepares a coupon for validation and application to an order.
 *
 * @param $coupon
 *   A raw coupon object.
 * @param $discounts
 *   An associative array of the discounts to be applied, keyed by nid or -lid. Or a string
 *   containing a message indicating why there are no discounts available.
 *
 * @return
 *   A fully validated coupon object with all additional properties set.  This is returned
 *   for convenience, as the $coupon provided is passed by reference and modified directly.
 *
 * @see uc_coupon_validate().
 */
function uc_coupon_prepare($coupon, $code, $discounts) {
  $coupon->code = $code;
  $coupon->valid = TRUE;
  $coupon->amount = 0;
  $coupon->pretax_amount = 0;
  if (!is_array($discounts)) {
    $coupon->discounts = array();
    $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));
    unset($coupon->message);
  }

  // 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,
  ));
  return $coupon;
}

/**
 * Implements hook_uc_coupon_validate().
 *
 * We implement our own hook to allow other modules a chance to run before us.
 *
 * @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) {

  // Coupons which produce no discount are not valid unless they are store credit
  // type, or have no face value (e.g. free shipping).
  if ($coupon->type !== 'credit' && $coupon->value != 0 && $coupon->amount == 0) {
    $coupon->valid = FALSE;
    return !empty($coupon->message) ? $coupon->message : t('This coupon is not applicable to your order.');
  }

  // 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.');
      }
    }
  }
  if ($coupon->type !== 'credit') {

    // 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.');
    }
  }
  else {
    if (!empty($coupon->usage['value']['codes'][$coupon->code]) && $coupon->usage['value']['codes'][$coupon->code] >= $coupon->value) {
      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.
 *
 * @return
 *   An array of discounts.
 */
function uc_coupon_calculate_discounts($coupon, $order) {

  // Can only calculate discounts if an order is provided.
  if (empty($order)) {
    return array();
  }
  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;
  }

  // Add in discounts for any included line items.
  $items = uc_order_load_line_items($order);
  if (!empty($order->line_items) && !empty($coupon->data['line_items'])) {
    foreach ($order->line_items as $line_item) {
      if (in_array($line_item['type'], $coupon->data['line_items'])) {

        // Use a negative id to distinguish this from a product discount.
        $lid = $line_item['line_item_id'];
        $lid = is_numeric($lid) ? -$lid : $lid;

        // No tax-inclusive line items in ubercart (yet).
        $included_rate = 1;

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

  // Calculate the discounts per item.
  $value = $coupon->value;
  if ($coupon->type === 'credit' && !empty($coupon->usage['value']['codes'][$coupon->code])) {
    $value -= $coupon->usage['value']['codes'][$coupon->code];
  }
  foreach ($discounts as $id => $discount) {
    $inclusive_price = $discount->price * $included_rates[$id];
    switch ($coupon->type) {
      case 'percentage':
        $discount->discount = $inclusive_price * $coupon->value / 100;
        break;
      case 'set_price':
        $discount->discount = max($inclusive_price - $coupon->value * $discount->qty, 0);
        break;
      default:
        if ($coupon->type === 'credit' || $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($value * ($inclusive_price / $total), $inclusive_price);
        }
        else {

          // Apply full discount value to each matching item.
          $discount->discount = min($value * $discount->qty, $inclusive_price);
        }
    }
    $discount->pretax_discount = $discount->discount / $included_rates[$id];
    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 $submit
 *     An array of additional options to attach to the form's submit elements (e.g. #ajax, #submit)
 */
function uc_coupon_table($coupons, $submit = 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 ($submit) {

      // Add ajax functionality to this table.
      $table[$i]['remove'] += $submit;
    }
    $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;
        if ($coupon->type === 'credit') {
          $credit = empty($coupon->usage['value']['codes'][$coupon->code]) ? 0 : $coupon->usage['value']['codes'][$coupon->code];
          $credit += empty($coupon->amount) ? 0 : $coupon->amount;
          $credit = $credit > $coupon->value ? 0 : $coupon->value - $credit;
          $options[$coupon->code] .= ' (' . t('@credit credit remaining', array(
            '@credit' => uc_currency_format($credit),
          )) . ')';
        }
      }
    }
  }
  return $options;
}
function uc_coupon_checkout_submit($form, &$form_state) {
  $form_state['rebuild'] = TRUE;
  unset($form_state['checkout_valid']);
  $form_state['redirect'] = 'cart/checkout';
}
function uc_coupon_order_submit($form, &$form_state) {
  $form_state['rebuild'] = TRUE;
  uc_coupon_form_submit($form, $form_state);
  $coupons = uc_coupon_get_order_coupons($form_state['order']);
  uc_coupon_apply_to_order($form_state['order'], uc_coupon_get_order_coupons($form_state['order']));

  // !TODO: Shouldn't save the order here because we prevent reverting changes
  // made by other panes?
  uc_order_save($form_state['order']);
}

/**
 * Form builder for the uc_coupon form.
 *
 * @param $context
 *     Where the form is to appear: 'cart', 'block' or 'checkout'
 * @param $submit
 *     An array of additional options to attach to the form's submit elements (e.g. #ajax, #submit)
 */
function uc_coupon_form($form, $form_state, $context = 'block', $submit = FALSE) {

  //dpm($form_state['order'], 'order build');
  $coupons = $context == 'order' ? uc_coupon_get_order_coupons($form_state['order']) : uc_coupon_session_validate();

  //dpm($coupons, 'coupons build');
  $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.');
    }
    drupal_add_css('#uc-coupon-active-coupons, #uc-coupon-other-discounts { clear: left; }', array(
      'type' => 'inline',
      'group' => CSS_DEFAULT,
    ));
    if ($submit) {
      $form['apply'] += $submit;
    }
  }

  // Add active coupons components (table and/or list).
  $options = _uc_coupon_options_list($coupons, $context != 'order');
  if (!empty($options)) {
    if (!empty($components['table'])) {
      $form['coupons_table'] = tapir_get_table('uc_coupon_table', $options, $submit);
    }
    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 ($submit) {
        $form['remove'] += $submit;
      }
    }
  }

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

/**
 * Implements hook_uc_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($order);
      uc_coupon_apply_to_order($order, $coupons);
    }

    // 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]);
      }
    }
  }
}

/**
 * Apply a set of coupons to an order.  
 * 
 * Line items and entries in the uc_coupons_orders table will be added for each coupon.  Any coupon line items
 * or entries which are not in the list of coupons will be removed.  Additionally, the coupons' discount
 * arrays will be added to the order object's data array.
 * 
 * @param $order
 *     The order to which the coupons should be applied.
 * @param $coupons
 *    An associative array of fully validated coupon objects, keyed by the coupon code.
 */
function uc_coupon_apply_to_order($order, $coupons) {

  // 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');
}

/**
 * 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']['quotes'])) {
    $commands[] = ajax_command_replace('#quotes-pane', drupal_render($form['panes']['quotes']));
  }
  if (isset($form['panes']['payment'])) {
    $commands[] = ajax_command_replace('#payment-pane', trim(drupal_render($form['panes']['payment'])));
  }
  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':

      // Revalidate the session coupons against the actual order.
      drupal_add_css('#coupon-messages { clear: both; }', array(
        'type' => 'inline',
        'group' => CSS_DEFAULT,
      ));
      $description = variable_get('uc_coupon_pane_description', t('Enter a coupon code for this order.'));
      $submit = array(
        '#limit_validation_errors' => array(),
        '#ajax' => array(
          'callback' => 'uc_coupon_checkout_update',
        ),
        '#submit' => array(
          'uc_coupon_checkout_submit',
        ),
      );
      $contents = uc_coupon_form(array(), $form_state, 'checkout', $submit);
      $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['panes']['coupon'], $form_state);
        return FALSE;

        // Prevent redirection.
      }
      else {

        // !TODO Coupon will not be submitted 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 a subarray if called from checkout or order page).
  switch ($context = $form['#uc_coupon_form_context']) {
    case 'checkout':
      $values = $form_state['values']['panes']['coupon'];
      break;
    case 'order':
      $values = $form_state['values']['coupon'];
      break;
    default:
      $values = $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)) {
      if ($context == 'order') {
        unset($form_state['order']->data['coupons']['code']);
      }
      else {
        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;
          if ($context == 'order') {
            unset($form_state['order']->data['coupons'][$code]);
          }
          else {
            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 = empty($values['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) {
      if ($context == 'order') {
        unset($form_state['order']->data['coupons']);
      }
      else {
        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)) {
      if ($context == 'order') {
        $coupon = uc_coupon_validate($code, $form_state['order'], user_load($form_state['order']->uid));
        if ($coupon->valid) {
          $form_state['order']->data['coupons'][$code] = $coupon->discounts;
        }
        else {
          drupal_set_message($coupon->message, 'error');
        }
      }
      else {
        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' => FALSE,
    'calculated' => TRUE,
  );
  return $items;
}

/**
 * Handle tax on coupons by calculating tax for individual discounted prices.
 */
function uc_coupon_tax_adjustment($price, $order, $tax) {
  $amount = 0;
  if (isset($order->data['coupons'])) {
    foreach ($order->data['coupons'] as $discounts) {
      foreach ($discounts as $id => $item) {
        if (is_numeric($id) && $id > 0) {

          // This is a product discount, so see if the product is taxable.
          $node = node_load($id);
          $adjust = in_array($node->type, $tax->taxed_product_types) && ($tax->shippable == 0 || $node->shippable == 1);
        }
        else {

          // This is a line-item discount, so find the corresponding line item.
          $lid = is_numeric($id) ? -$id : $id;

          // Convert id to a line item id.
          foreach ($order->line_items as $line_item) {
            if ($line_item['line_item_id'] == $lid) {
              $adjust = in_array($line_item['type'], $tax->taxed_line_items);
              break;
            }
          }
        }
        if ($adjust) {
          $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) {

  // Validate all codes in the session against the cart contents.
  $order = new UcOrder();
  $order->products = $items;
  $order->data = array();
  $coupons = uc_coupon_session_validate($order);
  if (variable_get('uc_coupon_show_in_cart', TRUE) && !empty($coupons)) {

    // If there are some valid coupons, then add them to the cart (but only if
    // they have a non-zero value.
    foreach ($coupons as $code => $coupon) {
      if ($coupon->amount != 0) {
        $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) {

  // Exclude any line-item discounts from the amount shown in the cart.
  $amount = 0;
  foreach ($coupon->discounts as $id => $discount) {
    if (is_numeric($id) && $id > 0) {
      $amount += $discount->discount;
    }
  }

  // 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' => -$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) {
      if ($coupon->amount != 0) {
        $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'];

  // Create variables for each user-added field.
  $fields = field_info_fields();
  foreach ($fields as $name => $field) {
    if (in_array('uc_coupon', array_keys($field['bundles']))) {
      $items = field_get_items('uc_coupon', $coupon, $name);
      $variables[$name] = $items;
    }
  }
  $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();
  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;
}

/**
 * Implements hook_entity_info();
 */
function uc_coupon_entity_info() {
  return array(
    'uc_coupon' => array(
      'label' => t('Coupon'),
      'controller class' => 'UcCouponController',
      'metadata controller class' => 'UcCouponMetadataController',
      'base table' => 'uc_coupons',
      'fieldable' => TRUE,
      'entity keys' => array(
        'id' => 'cid',
      ),
      'bundles' => array(
        'uc_coupon' => array(
          'label' => t('Coupon'),
          'admin' => array(
            'path' => 'admin/store/settings/coupon',
            'access arguments' => array(
              'manage store coupons',
            ),
          ),
        ),
      ),
      'view modes' => array(
        'full' => array(
          'label' => t('Administrative view'),
        ),
      ),
    ),
  );
}

/**
 * Loads one coupon entity from the database.
 */
function uc_coupon_load($cid, $reset = FALSE) {
  if (is_null($cid) || $cid < 1) {
    return FALSE;
  }
  $coupons = uc_coupon_load_multiple(array(
    $cid,
  ), array(), $reset);
  return $coupons ? reset($coupons) : FALSE;
}

/**
 * Loads one or more coupon entities from the database.
 *
 * @param $ids
 *   An array of coupon IDs.
 * @param $conditions
 *   An array of conditions on the {uc_coupons} table in the form
 *  'field' => $value.
 *
 * @return
 *   An array of order objects indexed by order_id.
 */
function uc_coupon_load_multiple($ids, $conditions = array(), $reset = FALSE) {
  return entity_load('uc_coupon', $ids, $conditions, $reset);
}

/**
 * Save a coupon object.
 *
 * If the 'cid' field is set, then this will update an existing coupon.
 * Otherwise, a new bulk seed will be generated, the coupon will be
 * inserted into the database, and $coupon->cid will be set.
 *
 * @param $coupon
 *   The coupon to save.
 *
 * @param $edit
 *   An optional array of extra data that other modules may need to save.
 */
function uc_coupon_save(&$coupon, $edit = array()) {
  entity_save('uc_coupon', $coupon);
}

/**
 * Delete a coupon object.
 *
 * @param $cid
 *   The id of the coupon to delete.
 */
function uc_coupon_delete($cid) {
  entity_delete('uc_coupon', $cid);
}

/**
 * Implements hook_field_extra_fields().
 */
function uc_coupon_field_extra_fields() {
  $extra = array();
  $extra['uc_coupon']['uc_coupon']['display']['admin_summary'] = array(
    'label' => t('Administrative Summary'),
    'description' => t('A summary of all coupon details.'),
    'weight' => 0,
  );
  return $extra;
}

/**
 * Implements hook_uc_order_pane().
 *
 * Defines the shipping quote order pane.
 */
function uc_coupon_uc_order_pane() {
  $panes['coupon'] = array(
    'callback' => 'uc_order_pane_coupon',
    'title' => t('Coupon, Credit or Discount Codes'),
    'desc' => t('Apply a coupon or discount code to the current order.'),
    'class' => 'pos-left',
    'weight' => 7,
    'show' => array(
      'edit',
    ),
  );
  return $panes;
}

/**
 * Coupon order pane callback.
 *
 * @see uc_quote_order_pane_quotes_submit()
 * @see uc_quote_apply_quote_to_order()
 */
function uc_order_pane_coupon($op, $order, &$form = NULL, &$form_state = NULL) {
  switch ($op) {
    case 'edit-form':
      $submit = array(
        '#limit_validation_errors' => array(
          array(
            'coupon',
          ),
        ),
        '#submit' => array(
          'uc_coupon_order_submit',
        ),
      );
      $form['coupon'] = uc_coupon_form(array(), $form_state, 'order', $submit);
      $form['#uc_coupon_form_context'] = 'order';
      $form['coupon']['#theme'] = 'uc_coupon_form';
      $form['coupon']['#tree'] = TRUE;
      break;
    case 'edit-theme':
      return drupal_render($form['coupon']);
  }
}

/**
 * Implements hook_form_uc_order_edit_form_alter().
 */
function uc_coupon_form_uc_order_edit_form_alter(&$form, &$form_state) {
  $order = $form_state['order'];
  $line_items = $order->line_items;
  foreach ($line_items as $item) {

    // Coupon line items should be changed using the coupon order-edit pane.
    if ($item['type'] == 'coupon') {
      $form['line_items'][$item['line_item_id']]['title'] = array(
        '#markup' => check_plain($item['title']),
      );
      $form['line_items'][$item['line_item_id']]['remove']['#access'] = FALSE;
      $form['line_items'][$item['line_item_id']]['amount'] = array(
        '#theme' => 'uc_price',
        '#price' => $item['amount'],
      );
    }
  }
}

/**
 * Gets the fully validated coupon objects that have been applied to this order.
 *
 * @param $order
 *   The order in question.
 * @param $recalculate
 *   If TRUE, the value of each coupon will be recalculated using the current state
 *   of the order and current coupon settings.
 *   If FALSE (default), the original coupon values will be preserved.
 */
function uc_coupon_get_order_coupons($order, $recalculate = FALSE) {
  $coupons = array();
  if (!empty($order->data['coupons'])) {
    if ($recalculate) {
      $dummy_order = clone $order;
      $dummy_order->data['coupons'] = array();
    }
    foreach ($order->data['coupons'] as $code => $discounts) {
      $coupon = uc_coupon_find($code);
      if (!empty($coupon->cid)) {
        if ($recalculate) {
          $discounts = uc_coupon_calculate_discounts($coupon, $dummy_order);
          $dummy_order->data['coupons'][$code] = $coupon->discounts;
        }
        $coupons[] = uc_coupon_prepare($coupon, $code, $discounts);
      }
    }
  }
  return $coupons;
}

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_apply_to_order Apply a set of coupons to an order.
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_delete Delete a coupon object.
uc_coupon_entity_info Implements hook_entity_info();
uc_coupon_field_extra_fields Implements hook_field_extra_fields().
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_form_uc_order_edit_form_alter Implements hook_form_uc_order_edit_form_alter().
uc_coupon_get_bulk_code Generate a single bulk coupon code.
uc_coupon_get_order_coupons Gets the fully validated coupon objects that have been applied to this order.
uc_coupon_init Implements hook_init().
uc_coupon_load Loads one coupon entity from the database.
uc_coupon_load_multiple Loads one or more coupon entities from the database.
uc_coupon_menu Implements hook_menu().
uc_coupon_order_submit
uc_coupon_permission Implements hook_permission().
uc_coupon_prepare Prepares a coupon for validation and application to an order.
uc_coupon_save Save a coupon object.
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.
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_order_pane Implements hook_uc_order_pane().
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_validate_multiple Validates a list of coupon codes against a specified order and account.
uc_coupon_views_api Implements hook_views_api().
uc_order_pane_coupon Coupon order pane callback.
_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