<?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
    LEFT JOIN {uc_orders} AS uo ON uco.oid = uo.order_id
    LEFT JOIN {uc_order_statuses} AS uos ON uo.order_status = uos.order_status_id
    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
    LEFT JOIN {uc_orders} AS uo ON uco.oid = uo.order_id
    LEFT JOIN {uc_order_statuses} AS uos ON uo.order_status = uos.order_status_id
    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 (float) $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}
    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}
    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($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;
          }
        }
      }
    }
  }

  // If no argument specified and the cache has not been built, we rebuild the cart to force validation.
  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_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));
      }
    }
    // Otherwise see if it's a checkbox submission.
    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])));
      }
    }
  }
  // Otherwise try to apply.
  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;
}
