You are here

uc_discounts.module in Ubercart Discounts (Alternative) 6.2

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

File

uc_discounts/uc_discounts.module
View source
<?php

//$Id: uc_discounts.module,v 1.29.2.31 2010/12/30 19:37:21 jrust Exp $

/**
 * @file
 * Discounts module for Ubercart 2.0+.
 *
 * Allows discount rules to be set up and applied to orders.
 */
module_load_include('inc', 'uc_discounts', 'uc_discounts.ca');
define("QUALIFYING_TYPE_MINIMUM_PRICE", 1);
define("QUALIFYING_TYPE_MINIMUM_QUANTITY", 2);
define("DISCOUNT_TYPE_FREE_ITEMS", 1);
define("DISCOUNT_TYPE_PERCENTAGE_OFF", 2);
define("DISCOUNT_TYPE_FIXED_AMOUNT_OFF", 3);
define("DISCOUNT_TYPE_FIXED_AMOUNT_OFF_PER_QUALIFYING_ITEM", 4);
define("DISCOUNT_TYPE_PERCENTAGE_OFF_PER_QUALIFYING_ITEM", 5);
define("FILTER_TYPE_NONE", 0);
define("FILTER_TYPE_PRODUCTS", 1);
define("FILTER_TYPE_TERMS", 2);
define("FILTER_TYPE_SKUS", 3);
define("FILTER_TYPE_CLASS", 4);
define("FILTER_TYPE_AUTHORS", 5);
define('DISCOUNT_FILTER_GROUPING_APPLICATION', 1);
define('DISCOUNT_FILTER_GROUPING_QUALIFICATION', 2);
define("ALL_PRODUCTS", "-1");
define("ALL_TERMS", "-1");
define("ALL_SKUS", "");
define("ALL_CLASSES", "");
define("ALL_ROLES", "-1");
define("ALL_AUTHORS", "-1");
define("IS_ACTIVE", TRUE);
define("LINE_ITEM_KEY_NAME", "uc_discounts");

// (1 to follow subtotal)
define("LINE_ITEM_WEIGHT", 1);
define("CALCULATE_DISCOUNT_RESPONSE_LINE_ITEMS_KEY", "line_items");
define("CALCULATE_DISCOUNT_RESPONSE_ERRORS_KEY", "errors");
define("CALCULATE_DISCOUNT_RESPONSE_MESSAGES_KEY", "messages");

///////////////////////////////////////////////////////////////////

//Drupal Hooks

///////////////////////////////////////////////////////////////////

/**
 * Implementation of hook_init().
 */
function uc_discounts_init() {
  drupal_add_css(drupal_get_path('module', 'uc_discounts') . '/uc_discounts.css');

  // @see uc_discounts_add_to_cart()
  if (isset($_SESSION['uc_discounts_after_add_to_cart'])) {
    $nid = $_SESSION['uc_discounts_after_add_to_cart']['nid'];
    $added_qty = $_SESSION['uc_discounts_after_add_to_cart']['qty'];
    unset($_SESSION['uc_discounts_after_add_to_cart']);
    $items_to_add = $new_product_ids = array();

    // Discounts are checked against the product(s) specified in Discount Application,
    // not Discount Qualifications. Therefore, we have to look up the discount by its
    // required_product_type field and check for discounts on the product(s) associated
    // with it
    $product = node_load($nid);
    $result = db_query("SELECT * FROM {uc_discounts} d\n                        WHERE d.add_to_cart = 1\n                          AND (d.has_activation = 0 OR d.activates_on < %d)\n                          AND (d.has_expiration = 0 OR d.expiration > %d)\n                          AND d.is_active = 1\n                          AND d.discount_type = %d\n                        ORDER BY weight", time(), time(), DISCOUNT_TYPE_FREE_ITEMS);
    while ($discount = db_fetch_object($result)) {
      if (in_array($product->nid, get_product_ids_for_discount_object($discount, DISCOUNT_FILTER_GROUPING_QUALIFICATION, TRUE))) {
        $new_product_ids = get_product_ids_for_discount_object($discount, DISCOUNT_FILTER_GROUPING_APPLICATION, TRUE);
        break;
      }
    }
    if (!empty($new_product_ids)) {
      foreach ($new_product_ids as $id) {
        $potential_product = node_load($id);

        // How many of the item are in the cart? We're only interested in node IDs right now.
        $items = uc_cart_get_contents();
        $qty = 0;
        $source_qty = 0;
        foreach ($items as $item) {
          if ($item->nid == $potential_product->nid) {
            $qty += $item->qty;
          }
          if ($item->nid == $product->nid) {
            $source_qty += $item->qty;
          }
        }
        $target_qty = $discount->discount_amount;
        $qualifying_amount = $discount->qualifying_amount;
        $times_applied = floor($qty / $target_qty);
        $times_to_apply = $target_qty / $qualifying_amount * $added_qty / $target_qty;

        // Make sure max_times_applied is respected
        if ($times_applied < $discount->max_times_applied || $discount->max_times_applied == 0) {

          // Calculate how many there should be and subtract what we already have
          $to_add = $target_qty * ($discount->max_times_applied ? min($discount->max_times_applied - $times_applied, $times_to_apply) : $times_to_apply);
        }

        // Don't add items immediately so that uc_cart_get_contents() will behave predictably.
        if ($to_add > 0) {
          $items_to_add[] = array(
            'nid' => $potential_product->nid,
            'qty' => $to_add,
            'data' => array(),
          );
        }
      }
    }
    foreach ($items_to_add as $p) {
      uc_cart_add_item($p['nid'], $p['qty'], $p['data'] + module_invoke_all('add_to_cart_data', $p), NULL, FALSE, FALSE, FALSE);
    }
  }
}

/**
 * Implementation of hook_perm().
 */
function uc_discounts_perm() {
  return array(
    'configure discounts',
  );
}

/**
 * Implementation of hook_menu().
 */
function uc_discounts_menu() {
  $items = array();
  $items['admin/store/uc_discounts'] = array(
    'title' => 'Discounts',
    'description' => 'Add and review discounts.',
    'page callback' => 'uc_discounts_admin_list',
    'access arguments' => array(
      'configure discounts',
    ),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'uc_discounts.admin.inc',
  );
  $items['admin/store/uc_discounts/list'] = array(
    'title' => 'List',
    'description' => 'View list of discounts.',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => 0,
  );
  $items['admin/store/uc_discounts/add'] = array(
    'title' => 'Add discount',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'uc_discounts_form',
    ),
    'access arguments' => array(
      'configure discounts',
    ),
    'type' => MENU_LOCAL_TASK,
    'weight' => 1,
    'file' => 'uc_discounts.admin.inc',
  );
  $items['admin/store/uc_discounts/edit/%'] = array(
    'title' => 'Edit discount rule',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'uc_discounts_form',
      4,
    ),
    'access arguments' => array(
      'configure discounts',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'uc_discounts.admin.inc',
  );
  $items['admin/store/uc_discounts/copy/%'] = array(
    'page callback' => 'uc_discounts_copy',
    'page arguments' => array(
      4,
    ),
    'access arguments' => array(
      'configure discounts',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'uc_discounts.admin.inc',
  );
  $items['admin/store/uc_discounts/delete/%'] = array(
    'title' => 'Delete discount rule',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'uc_discounts_delete',
      4,
    ),
    'access arguments' => array(
      'configure discounts',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'uc_discounts.admin.inc',
  );
  $items['admin/store/uc_discounts/generate_codes/%'] = array(
    'title' => 'Generate codes',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'uc_discounts_generate_codes_form',
      4,
    ),
    'access arguments' => array(
      'configure discounts',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'uc_discounts.admin.inc',
    'weight' => 2,
  );
  $items['cart/checkout/uc_discounts/calculate'] = array(
    'page callback' => 'uc_discounts_js_calculate',
    'access arguments' => array(
      'access content',
    ),
    'type' => MENU_CALLBACK,
  );
  $items['admin/reports/uc_discounts/all'] = array(
    'title' => 'Ubercart discounts usage data',
    'description' => 'View usage data for each Ubercart discount.',
    'page callback' => 'uc_discounts_report',
    'access arguments' => array(
      'access site reports',
    ),
    'file' => 'uc_discounts.admin.inc',
  );
  $items['admin/reports/uc_discounts/all/download'] = array(
    'title' => 'Download Ubercart discounts usage data',
    'description' => 'Download usage data for each Ubercart discount.',
    'page callback' => 'uc_discounts_report',
    'access arguments' => array(
      'access site reports',
    ),
    'file' => 'uc_discounts.admin.inc',
    'type' => MENU_CALLBACK,
  );
  $items['admin/reports/uc_discounts/discount'] = array(
    'title' => 'Ubercart discount usage data',
    'page callback' => 'uc_discounts_report_for_discount',
    'access arguments' => array(
      'access site reports',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'uc_discounts.admin.inc',
  );
  return $items;
}

/**
 * Implementation of hook_order().
 *
 * Manages order->uc_discounts_codes (array of code) and order->discounts (array of uses)
 */
function uc_discounts_order($op, &$arg1, $arg2) {
  switch ($op) {
    case 'load':

      //Get order's codes from database
      $arg1->uc_discounts_codes = uc_discounts_get_codes_for_order($arg1->order_id);
      break;
    case 'save':

      //If discount line items need updating
      if ($arg1->uc_discounts_line_items_need_updating) {

        //Delete existing order codes
        uc_discounts_order_codes_delete($arg1->order_id);

        //Save order's codes as string
        $codes_string = uc_discounts_codes_to_str($arg1->uc_discounts_codes);
        $new_discount_order_code = array(
          'order_id' => $arg1->order_id,
          'codes' => $codes_string,
        );
        drupal_write_record('uc_discounts_order_codes', $new_discount_order_code);

        //Get order line items
        if (is_array($arg1->line_items)) {
          $existing_line_items = $arg1->line_items;
        }
        else {
          $existing_line_items = uc_order_load_line_items($arg1, TRUE);
        }

        // Use new_order_line_items to populate $arg1->line_items by:
        // Storing all non-discounts line items
        // Storing new discount line items
        $new_order_line_items = array();

        //Delete existing discount line items
        foreach ($existing_line_items as $line_item) {
          if ($line_item["type"] == LINE_ITEM_KEY_NAME) {
            uc_order_delete_line_item($line_item["line_item_id"]);
          }
          else {
            $new_order_line_items[] = $line_item;
          }
        }

        //Add discount line items
        foreach ($arg1->uc_discounts_line_items as $line_item) {
          if ($line_item['amount'] != 0) {
            uc_order_line_item_add($arg1->order_id, $line_item['type'], $line_item['title'], $line_item['amount'], $line_item['weight'], $line_item['data']);
            $new_order_line_items[] = $line_item;
          }
        }

        //Update order line items (see new_order_line_items notes above)
        $arg1->line_items = $new_order_line_items;

        //Force tax recalculation (currently unused but may be required if line item weights change)

        //module_invoke("uc_taxes", "order", "save", $arg1, "");
        $arg1->uc_discounts_line_items_need_updating = FALSE;
      }
      break;
    case 'update':

      //If status changes to "cancelled", delete order uses
      if ($arg2 == "cancelled") {
        uc_discounts_uses_delete_for_order($arg1->order_id);
      }
      break;

    // Ensure stored discounts are accurate (recalculate and match new amounts against stored amounts)
    case 'submit':
      $result = uc_discounts_apply($arg1, FALSE);
      if (!$result['success']) {
        return array(
          array(
            'pass' => FALSE,
            'message' => $result['message'],
          ),
        );
      }
      break;
    case 'delete':

      // Delete existing order codes
      uc_discounts_order_codes_delete($arg1->order_id);

      //TO DO: determine if uses should be deleted or put global setting in for user to decide
      break;
  }
}

/**
 * Implementation of hook_uc_checkout_complete().
 * Upon successful completion of checkout, record usage
 * Note: $order->uc_discounts_codes is set by the checkout pane
 */
function uc_discounts_uc_checkout_complete($order, $account) {
  uc_discounts_uses_save_for_order($order);

  // Don't allow user to re-use codes on another order without re-entering them
  unset($_SESSION['uc_discounts_codes']);
}

/**
 * Implementation of hook_add_to_cart()
 */
function uc_discounts_add_to_cart($nid, $qty, $data) {

  // @see hook_init()
  $_SESSION['uc_discounts_after_add_to_cart'] = array(
    'nid' => $nid,
    'qty' => $qty,
  );
}

/**
 * Implementation of Ubercart's hook_line_item().
 *
 * Displays all discounts as a single line item
 *
 * @see hook_line_item()
 */
function uc_discounts_line_item() {
  $line_items[] = array(
    "id" => LINE_ITEM_KEY_NAME,
    "title" => t("Discount"),
    "weight" => LINE_ITEM_WEIGHT,
    "stored" => TRUE,
    // Added to total
    "calculated" => TRUE,
    "display_only" => FALSE,
  );
  return $line_items;
}

/**
 * Implementation of hook_order_pane
 */
function uc_discounts_order_pane() {
  $panes[] = array(
    'id' => 'uc_discounts',
    'callback' => 'uc_discounts_order_pane_callback',
    'title' => t('Discount codes'),
    'weight' => 8,
    'show' => array(
      'edit',
    ),
  );
  return $panes;
}

/**
 * Callback from hook_order_pane
 */
function uc_discounts_order_pane_callback($op, $arg1) {
  switch ($op) {
    case 'edit-form':
      $form['uc-discounts']['uc-discounts-codes'] = array(
        '#type' => 'textarea',
        '#rows' => 3,
      );
      $form['uc-discounts']['uc-discounts-button'] = array(
        '#type' => 'submit',
        '#value' => t('Apply discounts'),
      );
      return $form;
    case 'edit-theme':
      $form_rendered = drupal_render($arg1['uc-discounts']);
      return $form_rendered;
    case 'edit-ops':
      return array(
        t('Apply discounts'),
      );
    case t('Apply discounts'):
      if ($order = uc_order_load($arg1['order_id'])) {
        $order->uc_discounts_codes = array_merge($order->uc_discounts_codes, uc_discounts_codes_to_array($arg1['uc-discounts-codes']));
        $result = uc_discounts_apply($order, TRUE, FALSE);
        drupal_set_message($result['message'], $result['success'] ? 'notice' : 'error');

        /*
        $has_code_errors = FALSE;
        $errors = array();
        $warnings = array();
        $discounts = get_discounts_for_order($order, $errors, $warnings);

        foreach ($errors as $error) {
          drupal_set_message($error, 'error');
        }
        foreach ($warnings as $warning) {
          drupal_set_message($warning, 'warning');
        }

        if (empty($errors)) {
          if (empty($discounts)) {
            drupal_set_message(t('The code(s) did not yield a discount for this order.'), 'warning');
          }
          else {
            add_discount_line_items_to_order($order, $discounts);
            $order->uc_discounts_line_items_need_updating = TRUE;
            uc_discounts_order('save', $order, NULL);
            uc_discounts_uses_save_for_order($order);
            drupal_set_message(t('Discount code(s) applied to order.'));
          }
        }
        */
      }
      break;
  }
}

/**
 * Implementation of hook_cart_pane().
 *
 * @see hook_cart_pane()
 */
function uc_discounts_cart_pane($items) {
  $panes[] = array(
    'id' => 'uc_discounts_pane',
    'title' => t('Discounts'),
    'enabled' => TRUE,
    'weight' => 1,
    'body' => !is_null($items) ? uc_discounts_cart_pane_output($items) : '',
  );
  return $panes;
}

/**
 * Adds the discounts to the cart page as line items via javascript
 */
function uc_discounts_cart_pane_output($items) {
  global $user;

  //Create phony order object to call to get_discounts_for_order
  $order = new stdClass();
  $order->uid = $user->uid;
  $order->products = $items;
  $errors = array();
  $warnings = array();
  $messages = array();
  $discounts = get_discounts_for_order($order, $errors, $warnings, $messages);

  //If there are no discounts, do not alter cart
  if (count($discounts) == 0) {
    return '';
  }

  //Calculate subtotal with discounts
  $subtotal = 0;
  if (is_array($items)) {
    foreach ($items as $item) {
      $subtotal += $item->price * $item->qty;
    }
  }
  $total_discount_amount = 0;
  if (is_array($discounts)) {
    foreach ($discounts as $discount) {
      $total_discount_amount += $discount->amount;
    }
  }
  $subtotal_including_discounts = $subtotal - $total_discount_amount;

  //Add total discount message
  $messages[] = "<strong>" . t("Total discount") . ":</strong>&nbsp;" . uc_currency_format($total_discount_amount);

  //Add new subtotal message
  $messages[] = "<strong>" . t("Subtotal including discounts") . ":</strong>&nbsp;" . uc_currency_format($subtotal_including_discounts);

  //Start row index at item count + 2 (1 for subtotal row in cart form, 1 more for our first row)
  $i = count($items) + 2;

  //Create table to hold discount messages
  $body = "<div class='uc-discounts-cart-pane-container'><table class='uc-discounts-cart-pane-table'>";
  foreach ($messages as $message) {
    $evenOddClass = $i % 2 == 0 ? "even" : "odd";
    $body .= sprintf("<tr class='%s'><td class='%s'>", $evenOddClass, "uc-discounts-cart-pane-table-cell") . $message . "</td></tr>";
    $i += 1;
  }

  //Close table
  $body .= "</table></div>";

  //Write table using javascript between items and cart form buttons
  drupal_add_js(sprintf('$(document).ready(function() { $("#cart-form-buttons").before("%s"); });', $body), 'inline');
}

/**
 * Implementation of hook_checkout_pane().
 *
 * @see hook_checkout_pane()
 */
function uc_discounts_checkout_pane() {
  $panes[] = array(
    "id" => "uc_discounts",
    "callback" => "uc_checkout_pane_discounts",
    'process' => TRUE,
    "title" => t("Enter discount codes"),
    "weight" => 5,
  );
  return $panes;
}

/**
 * Implementation of hook_form_FORM_ID_alter()
 * Ensures that javascript for adding discounts is added to checkout,
 * regardless of whether discounts pane is used.
 */
function uc_discounts_form_uc_cart_checkout_form_alter(&$form, $form_state) {
  drupal_add_js(array(
    'uc_discounts' => array(
      'url' => url('cart/checkout/uc_discounts/calculate'),
      'line_item_key_name' => LINE_ITEM_KEY_NAME,
      'line_item_weight' => LINE_ITEM_WEIGHT,
      'total_discount_text' => t('Total discount'),
      'calculate_discount_response_line_items_key' => CALCULATE_DISCOUNT_RESPONSE_LINE_ITEMS_KEY,
      'calculate_discount_response_errors_key' => CALCULATE_DISCOUNT_RESPONSE_ERRORS_KEY,
      'calculate_discount_response_messages_key' => CALCULATE_DISCOUNT_RESPONSE_MESSAGES_KEY,
      'progress_msg' => t('Calculating discounts...'),
      'no_codes_entered' => t('Please enter at least one code'),
      'no_applicable_discounts' => t('No applicable discounts'),
      'err_msg' => t('There were problems determining if any discounts apply.  Please try again shortly.\\nIf this does not resolve the issue, please call @phone to complete your order.', array(
        '@phone' => variable_get('uc_store_phone', NULL),
      )),
      'response_parse_err_msg' => t('Unable to parse response text: '),
    ),
  ), 'setting');
  drupal_add_js('$(document).ready(function(e) { uc_discountsOnLoad(e); });', 'inline');
  drupal_add_js('misc/progress.js');
  drupal_add_js(drupal_get_path('module', 'uc_discounts') . '/uc_discounts.js');
}

/**
 * Discounts checkout pane callback
 *
 * More information at http://www.ubercart.org/docs/developer/245/checkout
 */
function uc_checkout_pane_discounts($op, &$arg1, $arg2) {
  global $user;
  switch ($op) {
    case "view":
      $description = t("Enter discount codes in the box below (one per line).");

      //If viewing an existing order, load order's codes
      if (!empty($arg1->order_id)) {
        $codes_string = uc_discounts_codes_to_str(uc_discounts_get_codes_for_order($arg1->order_id));
      }
      $contents["uc-discounts-codes"] = array(
        "#type" => "textarea",
        "#default_value" => $codes_string,
        "#rows" => 5,
        "#prefix" => "<div class='discount-codes-wrapper'>",
        "#suffix" => "</div>",
      );
      $contents["uc-discounts-placeholder"] = array(
        "#type" => "hidden",
        "#prefix" => "<div class='uc-discounts-messages-container'>",
        "#suffix" => "</div>",
      );
      $contents["uc-discounts-button"] = array(
        "#type" => "button",
        "#value" => t("Click to calculate discounts"),
      );
      return array(
        "description" => $description,
        "contents" => $contents,
      );
    case "process":

      // Save form values from checkout pane in order ($arg1).
      $arg1->uc_discounts_codes = uc_discounts_codes_to_array($arg2['uc-discounts-codes']);
      drupal_alter('uc_discounts_codes', $arg1, 'pane_submit');
      $has_code_errors = FALSE;
      $errors = array();
      $warnings = array();
      $discounts = get_discounts_for_order($arg1, $errors, $warnings);

      // Make sure the recorded discount codes for the order were only the ones that were actually used
      // for a discount.
      $applied_codes = array();
      foreach ($discounts as $discount) {
        $applied_codes[] = $discount->code;
      }
      $arg1->uc_discounts_codes = $applied_codes;
      foreach ($errors as $error) {
        drupal_set_message($error, "error");
      }
      foreach ($warnings as $warning) {
        drupal_set_message(t('Warning: @warning', array(
          '@warning' => $warning,
        )), 'error');
      }

      //If there were errors, return FALSE
      if (!empty($errors)) {
        return FALSE;
      }

      //Add discount line items to order
      add_discount_line_items_to_order($arg1, $discounts);

      //Mark order as needing discount line items updated
      $arg1->uc_discounts_line_items_need_updating = TRUE;
      break;
  }
}

/**
 * Add discount line items to order
 *
 * Note: assumes discount objects are the result of a call to get_discounts_for_order()
 */
function add_discount_line_items_to_order(&$order, $discounts) {

  //Create line items for discounts and store in order's uc_discounts_line_items field
  $line_items = array();
  foreach ($discounts as $discount) {
    $line_item = array(
      'type' => LINE_ITEM_KEY_NAME,
      'title' => $discount->short_description,
      'amount' => -$discount->amount,
      'weight' => LINE_ITEM_WEIGHT,
      'data' => array(
        'discount_id' => $discount->discount_id,
      ),
    );
    $line_items[] = $line_item;
  }
  $order->uc_discounts_line_items = $line_items;
}

/**
 * AJAX callback for discounts calculation.
 *
 * Calculate discount for an order in the checkout page.
 */
function uc_discounts_js_calculate() {
  global $user;
  if (!empty($_SESSION["cart_order"])) {
    $order_id = $_SESSION['cart_order'];
    $order = uc_order_load($order_id);

    //If session order exists, use it
    if (is_null($order)) {
      print '{}';
      exit;
    }
  }
  else {
    $order = new stdClass();
    $order->uid = $user->uid;
    $order->products = uc_cart_get_contents();
  }
  $order->uc_discounts_codes = uc_discounts_codes_to_array($_POST['uc-discounts-codes']);
  drupal_alter('uc_discounts_codes', $order, 'js_calculate');
  $line_items = array();
  $errors = array();
  $warnings = array();
  $messages = array();
  $discounts = get_discounts_for_order($order, $errors, $warnings, $messages);
  $i = 0;

  // Session vars get used by conditional action
  $_SESSION['uc_discounts_codes'] = $order->uc_discounts_codes;
  foreach ($discounts as $discount) {
    if ($discount->amount != 0) {
      $line_item = array();
      $line_item["id"] = LINE_ITEM_KEY_NAME . $i++;
      $line_item["type"] = $discount->type;
      $line_item["title"] = $discount->title;
      $line_item["amount"] = -$discount->amount;
      $line_item["weight"] = $discount->weight;
      $line_items[] = $line_item;
    }
  }
  if (!empty($warnings)) {
    $warnings2 = array();
    foreach ($warnings as $warning) {
      $warnings2[] = t('Warning: @warning', array(
        '@warning' => $warning,
      ));
    }
    $errors = array_merge($errors, $warnings2);
  }
  $calculate_discount_response = array(
    CALCULATE_DISCOUNT_RESPONSE_LINE_ITEMS_KEY => $line_items,
    CALCULATE_DISCOUNT_RESPONSE_ERRORS_KEY => $errors,
    CALCULATE_DISCOUNT_RESPONSE_MESSAGES_KEY => $messages,
  );
  drupal_json($calculate_discount_response);
  exit;
}

/**
 * Implements hook_theme().
 */
function uc_discounts_theme() {
  return array(
    'uc_discounts_cart_checkout_table' => array(
      'arguments' => array(
        'form' => NULL,
      ),
    ),
  );
}

/**
 * Implementation of hook_checkout_pane_alter()
 * Get discounted price to show up on checkout, & review
 */
function uc_discounts_checkout_pane_alter(&$panes) {
  foreach ($panes as &$pane) {
    if ($pane['id'] == 'cart') {
      $pane['callback'] = 'uc_discounts_checkout_pane_content';
    }
  }
}
function uc_discounts_checkout_pane_content($op) {
  switch ($op) {
    case 'view':
      $contents['cart_review_table'] = array(
        '#value' => theme('uc_discounts_cart_checkout_table'),
        '#weight' => variable_get('uc_pane_cart_field_cart_weight', 2),
      );
      return array(
        'contents' => $contents,
        'next-button' => FALSE,
      );
    case 'review':
      $discount_amount = uc_discounts_get_discount_amount_for_order($op);
      $items = uc_cart_get_contents();
      $output = '<table>';
      $context = array(
        'revision' => 'themed',
        'type' => 'cart_item',
        'subject' => array(),
      );
      $total = 0;
      foreach ($items as $item) {
        $total += $item->price * $item->qty;
        $desc = check_plain($item->title) . uc_product_get_description($item);
        $price_info = array(
          'price' => $item->price,
          'qty' => $item->qty,
        );
        $context['subject'] = array(
          'cart' => $items,
          'cart_item' => $item,
          'node' => node_load($item->nid),
        );
        $output .= '<tr valign="top"><td>' . $item->qty . '&times;</td><td width="100%">' . $desc . '</td><td nowrap="nowrap">' . uc_price($price_info, $context) . '</td></tr>';
      }
      if ($discount_amount > 0) {
        $final_price = $total - $discount_amount;
        $output .= '<tr valign="top"><td colspan="2"><strong>' . t('Discount') . ': </strong></td><td nowrap="nowrap">' . uc_price($discount_amount, $context) . '</td></tr>';
        $output .= '<tr valign="top"><td colspan="2"><strong>' . t('Total') . ': </strong></td><td nowrap="nowrap"><b>' . uc_price($final_price, $context) . '</b></td></tr>';
      }
      $output .= '</table>';
      $review[] = $output;
      return $review;
  }
}
function theme_uc_discounts_cart_checkout_table($show_subtotal = TRUE) {
  $subtotal = 0;
  $discount_amount = uc_discounts_get_discount_amount_for_order('checkout');

  // Set up table header.
  $header = array(
    array(
      'data' => t('Qty'),
      'class' => 'qty',
    ),
    array(
      'data' => t('Products'),
      'class' => 'products',
    ),
    array(
      'data' => t('Price'),
      'class' => 'price',
    ),
  );
  $context = array();

  // Set up table rows.
  $contents = uc_cart_get_contents();
  foreach ($contents as $item) {
    $price_info = array(
      'price' => $item->price,
      'qty' => $item->qty,
    );
    $context['revision'] = 'altered';
    $context['type'] = 'cart_item';
    $context['subject'] = array(
      'cart' => $contents,
      'cart_item' => $item * $discount_amount,
      'node' => node_load($item->nid),
    );
    $total = uc_price($price_info, $context);
    $subtotal += $total;
    $description = check_plain($item->title) . uc_product_get_description($item);

    // Remove node from context to prevent the price from being altered.
    $context['revision'] = 'themed-original';
    $context['type'] = 'amount';
    unset($context['subject']);
    $rows[] = array(
      array(
        'data' => t('@qty&times;', array(
          '@qty' => $item->qty,
        )),
        'class' => 'qty',
      ),
      array(
        'data' => $description,
        'class' => 'products',
      ),
      array(
        'data' => uc_price($total, $context),
        'class' => 'price',
      ),
    );
  }
  if ($discount_amount > 0) {
    $rows[] = array(
      array(
        'data' => '',
      ),
      array(
        'data' => t('Discount:'),
        'align' => 'right',
      ),
      array(
        'data' => uc_price($discount_amount, $context),
        'class' => 'price',
      ),
    );
    $subtotal = $subtotal - $discount_amount;
  }

  // Add the subtotal as the final row.
  if ($show_subtotal) {
    $context = array(
      'revision' => 'themed-original',
      'type' => 'amount',
    );
    $rows[] = array(
      'data' => array(
        array(
          'data' => '<span id="subtotal-title">' . t('Subtotal:') . '</span> ' . uc_price($subtotal, $context),
          'colspan' => 3,
          'class' => 'subtotal',
        ),
      ),
      'class' => 'subtotal',
    );
  }
  return theme('table', $header, $rows, array(
    'class' => 'cart-review',
  ));
}

///////////////////////////////////////////////////////////////////

//Database operations

///////////////////////////////////////////////////////////////////

/**
 * Deletes a uc_discounts row and all dependencies.
 */
function uc_discounts_delete_all($discount) {
  foreach (module_implements('uc_discount') as $module) {
    $function = $module . '_uc_discount';
    $function('delete', $discount);
  }
  db_query("DELETE FROM {uc_discounts_uses} WHERE discount_id=%d", $discount->discount_id);
  db_query("DELETE FROM {uc_discounts_products} WHERE discount_id=%d", $discount->discount_id);
  db_query("DELETE FROM {uc_discounts_terms} WHERE discount_id=%d", $discount->discount_id);
  db_query("DELETE FROM {uc_discounts_skus} WHERE discount_id=%d", $discount->discount_id);
  db_query("DELETE FROM {uc_discounts_roles} WHERE discount_id=%d", $discount->discount_id);
  db_query("DELETE FROM {uc_discounts_codes} WHERE discount_id=%d", $discount->discount_id);
  db_query("DELETE FROM {uc_discounts_authors} WHERE discount_id=%d", $discount->discount_id);
  db_query("DELETE FROM {uc_discounts} WHERE discount_id=%d", $discount->discount_id);
}

/**
 * Loads a discount
 */
function uc_discounts_load($discount_id) {
  $discount = db_fetch_object(db_query("SELECT * FROM {uc_discounts} WHERE discount_id=%d", $discount_id));
  foreach (module_implements('uc_discount') as $module) {
    $function = $module . '_uc_discount';
    $function('load', $discount);
  }
  return $discount;
}

/**
 * Returns codes for discount.
 */
function get_codes_for_discount($discount_id) {
  $codes = array();
  $result = db_query('SELECT code FROM {uc_discounts_codes} WHERE discount_id = %d', $discount_id);
  while ($row = db_fetch_array($result)) {
    $codes[] = $row['code'];
  }
  return $codes;
}

/**
 * Deletes all uc_discounts_codes rows for a discount.
 */
function uc_discounts_codes_delete($discount_id) {
  $query = "DELETE FROM {uc_discounts_codes} WHERE discount_id=%d";
  db_query($query, $discount_id);
}

/**
 * Returns product_ids for discount object.
 * Note: this function correctly returns all products for term-based discounts.
 *
 * @param $discount object
 * @param $exclude_all_products bool
 * @param $grouping const
 *
 * @return array of product ids (nids)
 */
function get_product_ids_for_discount_object($discount, $grouping = DISCOUNT_FILTER_GROUPING_APPLICATION, $exclude_all_products = FALSE) {
  $filter = $grouping == DISCOUNT_FILTER_GROUPING_APPLICATION ? $discount->filter_type : $discount->required_product_type;
  switch ($filter) {
    case FILTER_TYPE_PRODUCTS:
      return get_product_ids_for_discount($discount->discount_id, $grouping, $exclude_all_products);
    case FILTER_TYPE_TERMS:
      $product_ids = array();

      //Get products for terms
      $terms = get_term_ids_for_discount($discount->discount_id, $grouping, $exclude_all_products);
      $query = 'SELECT DISTINCT p.nid FROM {uc_products} p';
      if (!empty($terms)) {
        $query .= ' INNER JOIN {term_node} tn ON p.nid=tn.nid
                    INNER JOIN {uc_discounts_terms} dt ON tn.tid=dt.term_id
                    WHERE dt.discount_id=%d';
      }
      $result = db_query($query, $discount->discount_id);
      while ($row = db_fetch_object($result)) {
        $product_ids[] = $row->nid;
      }
      return $product_ids;
    case FILTER_TYPE_SKUS:
      $skus = get_skus_for_discount($discount->discount_id, $grouping, $exclude_all_products);
      $query = 'SELECT DISTINCT p.nid FROM {uc_products} p';
      if (!empty($skus)) {
        $query .= ' INNER JOIN {uc_discounts_skus} ds ON p.model=ds.sku
                    WHERE ds.discount_id=%d';
      }
      $result = db_query($query, $discount->discount_id);
      while ($row = db_fetch_object($result)) {
        $product_ids[] = $row->nid;
      }
      return $product_ids;
    case FILTER_TYPE_CLASS:
      $classes = get_classes_for_discount($discount->discount_id, $grouping, $exclude_all_products);
      $query = 'SELECT DISTINCT n.nid FROM {node} n';
      if (!empty($classes)) {
        $query .= ' INNER JOIN {uc_discounts_classes} dcl ON n.type=dcl.class
                      WHERE dcl.discount_id=%d';
      }
      $result = db_query($query, $discount->discount_id);
      while ($row = db_fetch_object($result)) {
        $product_ids[] = $row->nid;
      }
      return $product_ids;
    case FILTER_TYPE_AUTHORS:
      $authors = get_author_ids_for_discount($discount->discount_id, $grouping, $exclude_all_products);
      $query = 'SELECT DISTINCT n.nid FROM {node} n';
      if (!empty($authors)) {
        $query .= ' INNER JOIN {uc_discounts_authors} dau ON n.uid=dau.author_id
                      WHERE dau.discount_id=%d';
      }
      $result = db_query($query, $discount->discount_id);
      while ($row = db_fetch_object($result)) {
        $product_ids[] = $row->nid;
      }
      return $product_ids;
  }
  return array();
}

/**
 * Returns product_ids for discount.
 * Note: this function does not check filter_type so a discount with filter_type other than
 *    FILTER_TYPE_PRODUCTS will return no values.
 *
 * @param $discount_id
 * @param $grouping
 * @param $exclude_all_products
 *
 * @return array of product ids (nids)
 */
function get_product_ids_for_discount($discount_id, $grouping, $exclude_all_products = FALSE) {
  $query = "SELECT product_id FROM {uc_discounts_products} WHERE discount_id = %d AND grouping = %d";
  $args = array(
    $discount_id,
    $grouping,
  );
  if ($exclude_all_products) {
    $query .= ' AND product_id <> %d';
    $args[] = ALL_PRODUCTS;
  }
  $result = db_query($query, $args);
  $ids = array();
  while ($row = db_fetch_array($result)) {
    $ids[] = $row["product_id"];
  }
  return $ids;
}

/**
 * Returns term_ids for discount.
 */
function get_term_ids_for_discount($discount_id, $grouping, $exclude_all_terms = FALSE) {
  $query = "SELECT term_id FROM {uc_discounts_terms} WHERE discount_id = %d AND grouping = %d";
  $args = array(
    $discount_id,
    $grouping,
  );
  if ($exclude_all_products) {
    $query .= ' AND term_id <> %d';
    $args[] = ALL_TERMS;
  }
  $result = db_query($query, $args);
  $ids = array();
  while ($row = db_fetch_array($result)) {
    $ids[] = $row["term_id"];
  }
  return $ids;
}

/**
 * Returns SKUs for discount.
 */
function get_skus_for_discount($discount_id, $grouping, $exclude_all_skus = FALSE) {
  $query = "SELECT sku FROM {uc_discounts_skus} WHERE discount_id = %d AND grouping = %d";
  $args = array(
    $discount_id,
    $grouping,
  );
  if ($exclude_all_products) {
    $query .= ' AND sku <> "%s"';
    $args[] = ALL_SKUS;
  }
  $result = db_query($query, $args);
  $ids = array();
  while ($row = db_fetch_array($result)) {
    $ids[] = $row["sku"];
  }
  return $ids;
}

/**
 * Returns Product Class names for discount.
 */
function get_classes_for_discount($discount_id, $grouping, $exclude_all_classes = FALSE) {
  $query = "SELECT class FROM {uc_discounts_classes} WHERE discount_id = %d AND grouping = %d";
  $args = array(
    $discount_id,
    $grouping,
  );
  if ($exclude_all_products) {
    $query .= ' AND class <> "%s"';
    $args[] = ALL_CLASSES;
  }
  $result = db_query($query, $args);
  $ids = array();
  while ($row = db_fetch_array($result)) {
    $ids[] = $row["class"];
  }
  return $ids;
}

/**
 * Returns author_ids for discount.
 */
function get_author_ids_for_discount($discount_id, $grouping, $exclude_all_authors = FALSE) {
  $query = "SELECT author_id FROM {uc_discounts_authors} WHERE discount_id = %d AND grouping = %d";
  $args = array(
    $discount_id,
    $grouping,
  );
  if ($exclude_all_products) {
    $query .= ' AND author_id <> %d';
    $args[] = ALL_AUTHORS;
  }
  $result = db_query($query, $args);
  $ids = array();
  while ($row = db_fetch_array($result)) {
    $ids[] = $row["author_id"];
  }
  return $ids;
}

/**
 * Returns role_ids for discount.
 */
function get_role_ids_for_discount($discount_id, $exclude_all_roles = FALSE) {
  $query = "SELECT role_id FROM {uc_discounts_roles} WHERE discount_id = %d";
  $args = array(
    $discount_id,
  );
  if ($exclude_all_products) {
    $query .= ' AND role_id <> %d';
    $args[] = ALL_ROLES;
  }
  $result = db_query($query, $args);
  $ids = array();
  while ($row = db_fetch_array($result)) {
    $ids[] = $row["role_id"];
  }
  return $ids;
}

/**
 * Deletes all uc_discounts_products rows for a discount.
 */
function uc_discounts_products_delete($discount_id) {
  $query = "DELETE FROM {uc_discounts_products} WHERE discount_id=%d";
  db_query($query, $discount_id);
}

/**
 * Deletes all uc_discounts_terms rows for a discount.
 */
function uc_discounts_terms_delete($discount_id) {
  $query = "DELETE FROM {uc_discounts_terms} WHERE discount_id=%d";
  db_query($query, $discount_id);
}

/**
 * Deletes all uc_discounts_skus rows for a discount.
 */
function uc_discounts_skus_delete($discount_id) {
  $query = "DELETE FROM {uc_discounts_skus} WHERE discount_id=%d";
  db_query($query, $discount_id);
}

/**
 * Deletes all uc_discounts_classes rows for a discount.
 */
function uc_discounts_classes_delete($discount_id) {
  $query = "DELETE FROM {uc_discounts_classes} WHERE discount_id=%d";
  db_query($query, $discount_id);
}

/**
 * Deletes all uc_discounts_authors rows for a discount.
 */
function uc_discounts_authors_delete($discount_id) {
  $query = "DELETE FROM {uc_discounts_authors} WHERE discount_id=%d";
  db_query($query, $discount_id);
}

/**
 * Deletes all uc_discounts_roles rows for a discount.
 */
function uc_discounts_roles_delete($discount_id) {
  $query = "DELETE FROM {uc_discounts_roles} WHERE discount_id=%d";
  db_query($query, $discount_id);
}

/**
 * Returns discounts for order.
 * Note: $order->uc_discounts_codes must be set
 *
 * @param $order Order to get discounts for
 * @param $errors Reference to array to add error messages to
 * @param $messages Reference to array to add success messages to
 *
 * @return array of discount objects
 */
function get_discounts_for_order($order, &$errors = NULL, &$warnings = NULL, &$messages = NULL) {

  // Product NIDS in cart => subtotal of individual item
  $order_product_id_subtotal_map = array();

  // Product NIDS in cart => quantity of individual item
  $order_product_id_quantity_map = array();

  // Product NIDS in cart
  $order_product_ids = array();

  // Product NIDS in cart=> bool
  $order_product_ids_set = array();

  // Product objects in cart
  $order_product_id_product_array_map = array();
  $order_subtotal = 0;

  //Create IN string of product node IDs in order
  if (is_array($order->products) && !empty($order->products)) {
    foreach ($order->products as $product) {
      $nid = $product->nid;
      $order_product_ids_set[$nid] = TRUE;
      if (is_array($product->data) && !empty($product->data['kit_id'])) {
        $kit_id = $product->data['kit_id'];
        $order_product_ids_set[$kit_id] = TRUE;
        $kits[$kit_id]['product_qty'] += $product->qty;
      }
      uc_discounts_add_to_existing_map_number_value($order_product_id_subtotal_map, $nid, $product->price * $product->qty);
      uc_discounts_add_to_existing_map_number_value($order_product_id_quantity_map, $nid, $product->qty);
      $a = $order_product_id_product_array_map[$nid];
      if (!is_array($a)) {
        $a = array();
      }
      $a[] = $product;
      $order_product_id_product_array_map[$nid] = $a;
      $order_subtotal += $product->price * $product->qty;
    }
    if (is_array($kits) && !empty($kits)) {
      foreach ($kits as $kit_id => $value) {
        $kit_node = node_load($kit_id);
        foreach ($kit_node->products as $product_in_kit) {
          $pik_nid = $product_in_kit->nid;
          foreach ($order->products as $key => $product) {
            if ($product->nid == $pik_nid && $product->data['kit_id'] == $kit_id) {
              $kits[$kit_id]['kit_qty'] = $product->qty / $product_in_kit->qty;
              break;
            }
          }
        }
        uc_discounts_add_to_existing_map_number_value($order_product_id_quantity_map, $kit_id, $kits[$kit_id]['kit_qty']);
      }
    }
  }

  // Populate product NID array with NIDs from the order
  $order_product_ids = array_keys($order_product_ids_set);
  $temp_product_ids = $order_product_ids;
  $temp_product_ids[] = ALL_PRODUCTS;
  $product_ids_clause = sprintf("d.filter_type<>%d OR dp.product_id IN(%s)", FILTER_TYPE_PRODUCTS, join(",", $temp_product_ids));

  //Create IN string of term TIDs in order
  $temp_term_ids = array();
  $temp_term_ids[] = ALL_TERMS;
  if (is_array($order->products) && !empty($order->products)) {

    //Get terms for order's products
    $result = db_query("SELECT DISTINCT tid FROM {term_node} WHERE nid IN(%s)", join(",", $order_product_ids));
    while ($row = db_fetch_array($result)) {
      $temp_term_ids[] = $row["tid"];
      $order_term_ids[] = $row["tid"];
    }
  }
  $term_ids_clause = sprintf("d.filter_type<>%d OR dt.term_id IN(%s)", FILTER_TYPE_TERMS, join(",", $temp_term_ids));

  //Create IN string of SKUs in order
  $temp_skus = array();
  $temp_skus[] = "'" . db_escape_string(ALL_SKUS) . "'";
  if (is_array($order->products) && !empty($order->products)) {

    //Get SKUs for order's products
    $result = db_query("SELECT DISTINCT model FROM {uc_products} WHERE nid IN(%s)", join(",", $order_product_ids));
    while ($row = db_fetch_array($result)) {
      $temp_skus[] = "'" . db_escape_string($row["model"]) . "'";
    }
  }
  $skus_clause = sprintf("d.filter_type<>%d OR ds.sku IN(%s)", FILTER_TYPE_SKUS, join(",", $temp_skus));

  //Create IN string of classes in order
  $temp_classes = array();
  $temp_classes[] = "'" . db_escape_string(ALL_CLASSES) . "'";
  if (is_array($order->products) && !empty($order->products)) {

    //Get classes for order's products
    $result = db_query("SELECT DISTINCT type FROM {node} WHERE nid IN(%s)", join(",", $order_product_ids));
    while ($row = db_fetch_array($result)) {
      $temp_classes[] = "'" . db_escape_string($row["type"]) . "'";
    }
  }
  $classes_clause = sprintf("d.filter_type<>%d OR dcl.class IN(%s)", FILTER_TYPE_CLASS, join(",", $temp_classes));

  //Create IN string of authors in order
  $temp_authors = array();
  $temp_authors[] = "'" . db_escape_string(ALL_AUTHORS) . "'";
  if (is_array($order->products) && !empty($order->products)) {

    //Get authors for order's products
    $result = db_query("SELECT DISTINCT uid FROM {node} WHERE nid IN(%s)", join(",", $order_product_ids));
    while ($row = db_fetch_array($result)) {
      $temp_authors[] = "'" . db_escape_string($row["uid"]) . "'";
    }
  }
  $authors_clause = sprintf("d.filter_type<>%d OR dau.author_id IN(%s)", FILTER_TYPE_AUTHORS, join(",", $temp_authors));

  //Create codes clause
  $escaped_codes_string = NULL;
  if (!empty($order->uc_discounts_codes)) {

    //Create IN string of product node IDs in order
    $escaped_codes = array();
    foreach ($order->uc_discounts_codes as $code) {
      $escaped_codes[] = "'" . db_escape_string($code) . "'";
    }
    $escaped_codes_string = join(",", $escaped_codes);
    $codes_clause = sprintf(" OR d.discount_id IN( SELECT discount_id FROM {uc_discounts_codes} WHERE code IN(%s) )", $escaped_codes_string);
  }
  else {
    $codes_clause = "";
  }

  //Create roles clause
  $auth_rid = $order->uid != 0 ? DRUPAL_AUTHENTICATED_RID : DRUPAL_ANONYMOUS_RID;
  $roles_clause = sprintf(" OR d.discount_id IN(SELECT dr.discount_id FROM {uc_discounts_roles} dr LEFT JOIN {users_roles} ur ON (dr.role_id=ur.rid AND ur.uid=%d) WHERE ur.uid IS NOT NULL OR dr.role_id=%d OR dr.role_id=%d)", $order->uid, ALL_ROLES, $auth_rid);
  $grouping = DISCOUNT_FILTER_GROUPING_APPLICATION;

  //Add warnings for expired discounts with codes (if necessary)
  if (!empty($order->uc_discounts_codes)) {
    $query = sprintf("SELECT DISTINCT d.*, dc.code code FROM {uc_discounts} d\n      LEFT JOIN {uc_discounts_products} dp ON d.discount_id=dp.discount_id AND dp.grouping = {$grouping}\n      LEFT JOIN {uc_discounts_terms} dt ON d.discount_id=dt.discount_id AND dt.grouping = {$grouping}\n      LEFT JOIN {uc_discounts_skus} ds ON d.discount_id=ds.discount_id AND ds.grouping = {$grouping}\n      LEFT JOIN {uc_discounts_classes} dcl ON d.discount_id=dcl.discount_id AND dcl.grouping = {$grouping}\n      LEFT JOIN {uc_discounts_authors} dau ON d.discount_id=dau.discount_id AND dau.grouping = {$grouping}\n      LEFT JOIN {uc_discounts_roles} dr ON d.discount_id=dr.discount_id\n      LEFT JOIN {uc_discounts_codes} dc ON d.discount_id=dc.discount_id\n      WHERE dc.code IN(%s)\n      AND (d.has_role_filter=0%s)\n      AND (%s)\n      AND (%s)\n      AND (%s)\n      AND (%s)\n      AND (%s)\n      AND (d.has_expiration<>0 AND d.expiration<=%d)\n      AND (d.is_active=%d)\n      ORDER BY weight", $escaped_codes_string, $roles_clause, $product_ids_clause, $term_ids_clause, $skus_clause, $classes_clause, $authors_clause, time(), IS_ACTIVE);
    $result = db_query($query);
    while ($discount = db_fetch_object($result)) {
      $warnings[] = t('The discount for code "@code" has expired.', array(
        "@code" => $discount->code,
      ));
    }
  }
  $query = sprintf("SELECT DISTINCT d.* FROM {uc_discounts} d\n    LEFT JOIN {uc_discounts_products} dp ON d.discount_id=dp.discount_id AND dp.grouping = {$grouping}\n    LEFT JOIN {uc_discounts_terms} dt ON d.discount_id=dt.discount_id AND dt.grouping = {$grouping}\n    LEFT JOIN {uc_discounts_skus} ds ON d.discount_id=ds.discount_id AND ds.grouping = {$grouping}\n    LEFT JOIN {uc_discounts_classes} dcl ON d.discount_id=dcl.discount_id AND dcl.grouping = {$grouping}\n    LEFT JOIN {uc_discounts_authors} dau ON d.discount_id=dau.discount_id AND dau.grouping = {$grouping}\n    WHERE (d.requires_code=0%s)\n    AND (d.has_role_filter=0%s)\n    AND (%s)\n    AND (%s)\n    AND (%s)\n    AND (%s)\n    AND (%s)\n    AND (d.has_activation=0 OR d.activates_on<%d)\n    AND (d.has_expiration=0 OR d.expiration>%d)\n    AND (d.is_active=%d)\n    ORDER BY weight", $codes_clause, $roles_clause, $product_ids_clause, $term_ids_clause, $skus_clause, $classes_clause, $authors_clause, time(), time(), IS_ACTIVE);
  $result = db_query($query);
  $total_discount_amount = 0;
  $discounts = array();

  // Appears to check if order qualifies for each discount then applies discount.  Functionality should be separated, no?
  while ($discount = db_fetch_object($result)) {
    foreach (module_implements('uc_discount') as $module) {
      $function = $module . '_uc_discount';
      $function('load', $discount, $order);
    }

    // In case the hook modified the discount
    if (!$discount->is_active) {
      continue;
    }

    //Get code for discount (if one exists)
    $discount->code = NULL;
    if (!empty($escaped_codes_string)) {
      $query = sprintf("SELECT code FROM {uc_discounts_codes} WHERE code IN(%s) AND discount_id=%d", $escaped_codes_string, $discount->discount_id);
      $row = db_fetch_array(db_query($query));
      if (!empty($row)) {
        $discount->code = $row["code"];
      }
    }

    //The query handled valid codes and expiration, this block must:

    //  check max uses (if applicable)
    //  check if discount is being combined and can be combined
    //  check if order qualifies (type, requires_single_product_to_qualify, required_products, can_be_combined_with_other_discounts)
    //  determine number of times to apply discount

    //If this discount has a max uses amount, check max uses
    if ($discount->max_uses > 0) {
      $row = db_fetch_array(db_query("SELECT COUNT(*) as uses_count FROM {uc_discounts_uses} WHERE discount_id=%d", $discount->discount_id));
      if ($row["uses_count"] >= $discount->max_uses) {

        //If this is a coded discount, add error message
        if (!is_null($warnings) && !is_null($discount->code)) {
          $warnings[] = t('The discount for code "@code" has reached its maximum number of uses.', array(
            "@code" => $discount->code,
          ));
        }
        continue;
      }
      $discount->uses_count = $row["uses_count"];
    }

    //If this discount has a max uses per user amount, check max uses per user
    if ($discount->max_uses_per_user > 0) {
      $row = db_fetch_array(db_query("SELECT COUNT(*) as user_uses_count FROM {uc_discounts_uses} WHERE discount_id=%d AND user_id=%d", $discount->discount_id, $order->uid));
      if ($row["user_uses_count"] >= $discount->max_uses_per_user) {

        //If this is a coded discount, add warning message
        if (!is_null($warnings) && !is_null($discount->code)) {
          $warnings[] = t('The discount for code "@code" has reached its maximum number of uses.', array(
            "@code" => $discount->code,
          ));
        }
        continue;
      }
      $discount->user_uses_count = $row["user_uses_count"];
    }

    //If code exists and this discount has a max uses per code amount, check max uses per code
    if (!is_null($discount->code) && $discount->max_uses_per_code > 0) {
      $row = db_fetch_array(db_query("SELECT COUNT(*) as code_uses_count FROM {uc_discounts_uses} WHERE discount_id=%d AND code='%s'", $discount->discount_id, $discount->code));
      if ($row["code_uses_count"] >= $discount->max_uses_per_code) {

        //Add warning message
        if (!is_null($warnings)) {
          $warnings[] = t('The discount code "@code" has reached its max number of uses.', array(
            "@code" => $discount->code,
          ));
        }
        continue;
      }
      $discount->code_uses_count = $row["code_uses_count"];
    }

    //If there are applied discounts, check if discount is being combined and can be combined
    if (count($discounts) > 0) {
      if (!$discount->can_be_combined_with_other_discounts) {

        //If this is a coded discount, add error message
        if (!is_null($warnings) && !is_null($discount->code)) {
          $warnings[] = t('The discount for code "@code" cannot be combined with other discounts.', array(
            "@code" => $discount->code,
          ));
        }
        continue;
      }

      // Check if the first discount can't be combined
      if (!$discounts[0]->can_be_combined_with_other_discounts) {

        //If first discount was a coded discount, add error message (only print warning if both

        //discounts have codes)
        if (!is_null($warnings) && !empty($discounts[0]->code) && !is_null($discount->code)) {
          $warnings[] = t('The discount for code "@code" cannot be combined with other discounts.', array(
            "@code" => $discounts[0]->code,
          ));
        }
        continue;
      }
    }

    //Check if order qualifies for this discount (check type, requires_single_product_to_qualify, required_products)

    //Get product IDs for determining discount application
    $discount_product_ids = get_product_ids_for_discount_object($discount);
    if (in_array(ALL_PRODUCTS, $discount_product_ids)) {
      $discount_product_ids = $order_product_ids;
    }

    // Get product IDs for determining discount qualification
    $required_product_ids = get_product_ids_for_discount_object($discount, DISCOUNT_FILTER_GROUPING_QUALIFICATION);
    $required_ids_in_order = array_intersect($required_product_ids, $order_product_ids);
    if (!empty($discount->code) && !empty($required_product_ids) && empty($required_ids_in_order)) {
      $warnings[] = t('The discount for code "@code" requires a product that is not in your cart.', array(
        '@code' => $discount->code,
      ));
      continue;
    }
    if ($discount->use_only_discounted_products_to_qualify) {
      $qualification_product_ids = $discount_product_ids;
    }
    elseif (!empty($required_product_ids)) {
      $qualification_product_ids = $required_product_ids;
    }
    else {
      $qualification_product_ids = $order_product_ids;
    }

    //Determine total qualifying amount of order (store in order_qualifying_amount)
    $order_qualifying_amount = 0;
    switch ($discount->qualifying_type) {
      case QUALIFYING_TYPE_MINIMUM_PRICE:

        //Determine the total subtotal of discount's products
        foreach ($qualification_product_ids as $product_id) {
          if (isset($order_product_id_subtotal_map[$product_id])) {
            if ($discount->requires_single_product_to_qualify) {
              if ($order_product_id_subtotal_map[$product_id] >= $order->qualifying_amount) {

                //In this case, $order_qualifying amount should be the sum of

                //prices of products that both qualify and meet the minimum

                //qualification amount based on their individual price.
                $order_qualifying_amount += $order_product_id_subtotal_map[$product_id];
              }
            }
            else {
              $order_qualifying_amount += $order_product_id_subtotal_map[$product_id];
            }
          }
        }

        //Subtract already discounted amount
        $order_qualifying_amount -= $total_discount_amount;
        break;
      case QUALIFYING_TYPE_MINIMUM_QUANTITY:

        //Determine the total quantity of discount's products
        foreach ($qualification_product_ids as $product_id) {
          if (isset($order_product_id_quantity_map[$product_id])) {
            if ($discount->requires_single_product_to_qualify) {
              if ($order_product_id_quantity_map[$product_id] >= $discount->qualifying_amount) {

                //In this case, $order_qualifying amount should be the sum of products that both qualify and meet the minimum qualification amount based on their quantity.
                $order_qualifying_amount += $order_product_id_quantity_map[$product_id];
              }
            }
            else {
              $order_qualifying_amount += $order_product_id_quantity_map[$product_id];
            }
          }
        }
        break;
    }

    //If order does not qualify for this discount
    if ($order_qualifying_amount < $discount->qualifying_amount) {

      //If this is a coded discount, add warning message
      if (!is_null($warnings) && !is_null($discount->code)) {
        switch ($discount->qualifying_type) {
          case QUALIFYING_TYPE_MINIMUM_PRICE:
            $qualifying_amount = uc_currency_format($discount->qualifying_amount);
            $warnings[] = t('The discount for code "@code" requires a minimum amount of @qualifying_amount to qualify.', array(
              "@code" => $discount->code,
              "@qualifying_amount" => $qualifying_amount,
            ));
            break;
          case QUALIFYING_TYPE_MINIMUM_QUANTITY:
            $warnings[] = t('The discount for code "@code" requires a minimum quantity of @qualifying_amount to qualify.', array(
              "@code" => $discount->code,
              "@qualifying_amount" => $discount->qualifying_amount,
            ));
            break;
        }
      }
      continue;
    }

    //If this discount has a maximum qualifying amount and the order exceeds it
    if ($discount->has_qualifying_amount_max && $order_qualifying_amount > $discount->qualifying_amount_max) {

      //If this is a coded discount, add error message
      if (!is_null($warnings) && !is_null($discount->code)) {
        $qualifying_amount_max = uc_currency_format($discount->qualifying_amount_max);
        switch ($discount->qualifying_type) {
          case QUALIFYING_TYPE_MINIMUM_PRICE:
            $warnings[] = t('The discount for code "@code" cannot exceed the price of @qualifying_amount_max to qualify.', array(
              "@code" => $discount->code,
              "@qualifying_amount_max" => $qualifying_amount_max,
            ));
            break;
          case QUALIFYING_TYPE_MINIMUM_QUANTITY:
            $warnings[] = t('The discount for code "@code" cannot exceed the quantity of @qualifying_amount_max to qualify.', array(
              "@code" => $discount->code,
              "@qualifying_amount_max" => $discount->qualifying_amount_max,
            ));
            break;
        }
      }
      continue;
    }

    //Get product IDs in order that are in discount
    $order_and_discount_product_ids = array_intersect($discount_product_ids, $order_product_ids);

    //Create array of product objects in cart to which this discount gets applied.
    $order_and_discount_products = array();
    foreach ($order_and_discount_product_ids as $product_id) {
      if (array_key_exists($product_id, $order_product_id_product_array_map)) {
        $order_and_discount_products = array_merge($order_and_discount_products, $order_product_id_product_array_map[$product_id]);
      }
    }

    // Amount of products to which discounts get applied
    $discount_products_amount = 0;

    // Quantity of products to which discounts get applied
    $discount_products_qty = 0;
    foreach ($order_and_discount_products as $product) {
      $discount_products_qty += $product->qty;
      $discount_products_amount += $product->qty * $product->price;
    }

    // Determine number of times to apply discount, by default once for every qualifying product
    $discount->times_applied = $discount_products_qty;

    // See if it should be limited based on number of required products in the cart
    if ($discount->limit_max_times_applied && !empty($required_product_ids)) {
      $times = 0;
      foreach ($required_product_ids as $id) {
        if (isset($order_product_id_quantity_map[$id])) {
          $times += $order_product_id_quantity_map[$id];
        }
      }
      $discount->times_applied = min($discount->times_applied, $times);
    }

    // See if we need to limit the number of applications with a hard cap
    if ($discount->max_times_applied != 0) {
      $discount->times_applied = min($discount->times_applied, $discount->max_times_applied);
    }
    switch ($discount->discount_type) {
      case DISCOUNT_TYPE_FREE_ITEMS:

        //The variable discount_amount is the monitary amount of discount
        $discount_amount = 0;

        //The variable free_items_remaining is the [max] number of free items for the order
        $free_items_remaining = $discount->discount_amount * $discount->times_applied;

        //Loop until all free items have been applied or there are no more products to

        //discount (discount cheapest first)
        while ($free_items_remaining > 0) {

          //Determine cheapest remaining qualifying item
          $cheapest_product = NULL;
          foreach ($order_and_discount_products as $product) {

            //If this product has been fully discounted, continue
            if ($product->uc_discounts_is_fully_discounted) {
              continue;
            }

            //If no current cheapest product exists, use this product
            if (is_null($cheapest_product)) {
              $cheapest_product = $product;
            }
            else {

              //If this product is cheaper than the current cheapest product,

              //use this product instead
              if ($product->price < $cheapest_product->price) {
                $cheapest_product = $product;
              }
            }
          }

          //If no cheapest product could be found, there are no more products to

          //discount, break
          if (is_null($cheapest_product)) {
            break;
          }

          //Discount up to the lesser of cheapest product quantity and free_items_remaining
          $discount_count = min($cheapest_product->qty, $free_items_remaining);

          //Add current discount amount to running total
          $discount_amount += $discount_count * $cheapest_product->price;

          //Mark item fully discounted
          $cheapest_product->uc_discounts_is_fully_discounted = TRUE;
          $free_items_remaining -= $discount_count;
        }
        $discount->amount = $discount_amount;
        break;
      case DISCOUNT_TYPE_PERCENTAGE_OFF_PER_QUALIFYING_ITEM:
        $discount->amount = $discount_products_amount / $discount_products_qty * $discount->discount_amount * $discount->times_applied;
        break;
      case DISCOUNT_TYPE_PERCENTAGE_OFF:

        // This is so complicated because we need to ensure only qualifying
        // products get discounted and no product is discounted more than 100%
        // Always apply once since it applies to the whole order
        $discount->times_applied = 1;

        //If this discount uses all products and previous discount is:

        //  same weight as this discount
        //  percentage off
        //  products of discounts must match

        //discount using same subtotal as last discount
        if (count($discounts) > 0) {
          $last_discount = $discounts[count($discounts) - 1];
          if ($last_discount->weight == $discount->weight && $last_discount->discount_type == DISCOUNT_TYPE_PERCENTAGE_OFF) {

            //Last discount's and this discount's products must match exactly
            $are_equal = TRUE;
            $last_discount_product_ids = get_product_ids_for_discount_object($last_discount);
            $this_discount_product_ids = get_product_ids_for_discount_object($discount);

            //If both contain "all products" they are equal
            if (in_array(ALL_PRODUCTS, $last_discount_product_ids) && in_array(ALL_PRODUCTS, $this_discount_product_ids)) {
              $are_equal = TRUE;
            }
            else {
              foreach ($this_discount_product_ids as $product_id) {
                if (!in_array($product_id, $last_discount_product_ids)) {
                  $are_equal = FALSE;
                  break;
                }
              }
              if ($are_equal) {
                foreach ($last_discount_product_ids as $product_id) {
                  if (!in_array($product_id, $this_discount_product_ids)) {
                    $are_equal = FALSE;
                    break;
                  }
                }
              }
            }
            if ($are_equal) {

              //($last_discount->amount / $last_discount->discount_amount) == last discount's subtotal
              $local_order_subtotal = $last_discount->amount / $last_discount->discount_amount;
              $discount->amount = $local_order_subtotal * $discount->discount_amount;
              break;
            }
          }
        }

        //Start patch from lutegrass:

        //This fixes the problem where a percent discount does not apply to all products

        //(but doesn't fix the problem where the products being discounted have already been discounted

        //in full, or the case where the cart consists only of the products included in this discount)

        // Get qualifying products -- ignore "all products" selection
        $discount_product_ids = get_product_ids_for_discount_object($discount, DISCOUNT_FILTER_GROUPING_APPLICATION, TRUE);

        // Do we have any products
        if (count($discount_product_ids) > 0) {
          $discounted_products_amount = 0;
          foreach ($order_and_discount_products as $product) {
            $discounted_products_amount += $product->price * $product->qty;
          }
          $discount->amount = $discounted_products_amount * $discount->discount_amount;

          // Discount the subtotal so far
        }
        else {
          $discount->amount = max($order_subtotal - $total_discount_amount, 0) * $discount->discount_amount;
        }

        //End patch from lutegrass
        break;
      case DISCOUNT_TYPE_FIXED_AMOUNT_OFF:

        // Always apply once since it applies to the whole order
        $discount->times_applied = 1;
        $discount->amount = $discount->discount_amount;
        break;
      case DISCOUNT_TYPE_FIXED_AMOUNT_OFF_PER_QUALIFYING_ITEM:
        $discount->amount = $discount->discount_amount * $discount->times_applied;
        break;
    }
    if (!is_null($messages)) {
      $options = array(
        "@short_description" => $discount->short_description,
        "@code" => $discount->code,
        "@times_applied" => $discount->times_applied,
        "@discount_amount" => uc_currency_format($discount->amount),
        "@time_string" => $time_string,
      );
      if (!is_null($discount->code)) {
        if (empty($discount->amount)) {
          $messages[] = t("The discount, '@short_description', with code '@code' was applied.", $options);
        }
        elseif ($discount->times_applied == 1) {
          $messages[] = t("The discount, '@short_description', with code '@code' was applied for a discount of @discount_amount.", $options);
        }
        else {
          $messages[] = t("The discount, '@short_description', with code '@code' was applied @times_applied times for a discount of @discount_amount.", $options);
        }
      }
      else {
        if (empty($discount->amount)) {
          $messages[] = t("The discount, '@short_description' was applied.", $options);
        }
        elseif ($discount->times_applied == 1) {
          $messages[] = t("The discount, '@short_description', was applied for a discount of @discount_amount.", $options);
        }
        else {
          $messages[] = t("The discount, '@short_description', was applied @times_applied times for a discount of @discount_amount.", $options);
        }
      }
    }

    //Round the discount to two places
    $discount->amount = round($discount->amount, 2);

    //Add this discount's amount to running total
    $total_discount_amount += $discount->amount;

    //Add this discount to list of discounts applied to order
    $discounts[] = $discount;
  }

  // If no discount array was filled in, means that the discount was not found in the database
  if (count($discounts) == 0 && !empty($order->uc_discounts_codes)) {
    $warnings[] = t('Coupon does not exist or is not valid.');
  }

  // end of db fetch while loop
  return $discounts;
}

/**
 * Helper function that gets the discount amount for the order
 * @return int *
 */
function uc_discounts_get_discount_amount_for_order($op) {
  global $user;
  $order = uc_order_load($_SESSION["cart_order"]);
  if (!$order) {
    $items = uc_cart_get_contents();
    $order = new stdClass();
    $order->uid = $user->uid;
    $order->products = $items;
  }
  $errors = array();
  $warnings = array();
  $messages = array();
  $discounts = get_discounts_for_order($order, $errors, $warnings, $messages);
  $total_discount_amount = 0;
  if (is_array($discounts)) {
    foreach ($discounts as $discount) {
      if (!$discount->requires_code || $op == 'review') {
        $total_discount_amount += $discount->amount;
      }
    }
  }
  return $total_discount_amount;
}

/**
 * Helper function that gets the codeless discounted price of a product
 * @param $product A node product
 * @param $return_null Return NULL for the price if there are no discounts.  Otherwise it returns the sell_price
 */
function uc_discounts_get_discounted_price_for_product($product, $return_null = TRUE) {
  $discounts = get_codeless_discounts_for_product_and_quantity($product, 1);
  if (empty($discounts)) {
    return $return_null ? NULL : $product->sell_price;
  }
  $total_discount_amount = 0;
  foreach ($discounts as $discount) {
    $total_discount_amount += $discount->amount;
  }
  return $product->sell_price - $total_discount_amount;
}

/**
 * Returns all codeless discounts for product.
 */
function get_codeless_discounts_for_product($product, $sort_column = "weight", $is_ascending_sort = TRUE) {
  return get_codeless_discounts_for_product_and_quantity($product, NULL, $sort_column, $is_ascending_sort);
}

/**
 * Returns all codeless discounts for product when specified quantity is purchased.
 *
 * @param $product_id Node ID for product
 */
function get_codeless_discounts_for_product_and_quantity($product, $quantity = NULL, $sort_column = "weight", $is_ascending_sort = TRUE) {
  if (is_null($product) || !$product) {
    return array();
  }

  //If quantity was specified
  if (!is_null($quantity)) {
    global $user;

    //Create phony order and get discounts for order
    $product->price = $product->sell_price;
    $product->qty = $quantity;
    $order = new stdClass();
    $order->uid = $user->uid;
    $order->products = array(
      $product,
    );
    return get_discounts_for_order($order);
  }

  //Otherwise make special query

  //Get terms for product
  $term_ids = array();
  $term_ids[] = ALL_TERMS;
  $result = db_query("SELECT DISTINCT tid FROM {term_node} WHERE nid=%d", $product->nid);
  while ($row = db_fetch_array($result)) {
    $term_ids[] = $row["tid"];
  }

  //Get SKUs for product
  $skus = array();
  $skus[] = "'" . db_escape_string(ALL_SKUS) . "'";
  $result = db_query("SELECT DISTINCT model FROM {uc_products} WHERE nid=%d", $product->nid);
  while ($row = db_fetch_array($result)) {
    $skus[] = "'" . db_escape_string($row["model"]) . "'";
  }

  //Get classes for product
  $classes = array();
  $classes[] = "'" . db_escape_string(ALL_CLASSES) . "'";
  $result = db_query("SELECT DISTINCT type FROM {node} WHERE nid=%d", $product->nid);
  while ($row = db_fetch_array($result)) {
    $classes[] = "'" . db_escape_string($row["type"]) . "'";
  }

  //Get uids for product
  $authors = array();
  $authors[] = "'" . db_escape_string(ALL_AUTHORS) . "'";
  $result = db_query("SELECT DISTINCT uid FROM {node} WHERE nid=%d", $product->nid);
  while ($row = db_fetch_array($result)) {
    $authors[] = "'" . db_escape_string($row["uid"]) . "'";
  }

  //Create roles clause
  global $user;
  $auth_rid = $user->uid != 0 ? DRUPAL_AUTHENTICATED_RID : DRUPAL_ANONYMOUS_RID;
  $roles_clause = sprintf(" OR d.discount_id IN(SELECT dr.discount_id FROM {uc_discounts_roles} dr" . ", {users_roles} ur WHERE (dr.role_id=ur.rid AND ur.uid=%d) OR dr.role_id=%d OR dr.role_id=%d)", $user->uid, ALL_ROLES, $auth_rid);
  $product_ids = array(
    ALL_PRODUCTS,
    $product->nid,
  );
  $product_ids_clause = sprintf("d.filter_type<>%d OR dp.product_id IN(%s)", FILTER_TYPE_PRODUCTS, join(",", $product_ids));
  $term_ids_clause = sprintf("d.filter_type<>%d OR dt.term_id IN(%s)", FILTER_TYPE_TERMS, join(",", $term_ids));
  $skus_clause = sprintf("d.filter_type<>%d OR ds.sku IN(%s)", FILTER_TYPE_SKUS, join(",", $skus));
  $classes_clause = sprintf("d.filter_type<>%d OR dcl.class IN(%s)", FILTER_TYPE_CLASS, join(",", $classes));
  $authors_clause = sprintf("d.filter_type<>%d OR dau.author_id IN(%s)", FILTER_TYPE_AUTHORS, join(",", $authors));
  $sort_order_string = is_ascending_sort ? "ASC" : "DESC";
  $grouping = DISCOUNT_FILTER_GROUPING_APPLICATION;
  $query = sprintf("SELECT d.* FROM {uc_discounts} d\n    LEFT JOIN {uc_discounts_products} dp ON d.discount_id = dp.discount_id AND dp.grouping = {$grouping}\n    LEFT JOIN {uc_discounts_terms} dt ON d.discount_id = dt.discount_id AND dt.grouping = {$grouping}\n    LEFT JOIN {uc_discounts_skus} ds ON d.discount_id = ds.discount_id AND ds.grouping = {$grouping}\n    LEFT JOIN {uc_discounts_classes} dcl ON d.discount_id = dcl.discount_id AND dcl.grouping = {$grouping}\n    LEFT JOIN {uc_discounts_authors} dau ON d.discount_id = dau.discount_id AND dau.grouping = {$grouping}\n    WHERE d.requires_code = 0\n    AND (d.has_role_filter = 0%s)\n    AND (%s)\n    AND (%s)\n    AND (%s)\n    AND (%s)\n    AND (%s)\n    AND (d.has_activation = 0 OR d.activates_on < %d)\n    AND (d.has_expiration = 0 OR d.expiration > %d)\n    AND (d.is_active = %d)\n    ORDER BY d.%s %s", $roles_clause, $product_ids_clause, $term_ids_clause, $skus_clause, $classes_clause, $authors_clause, time(), time(), IS_ACTIVE, $sort_column, $sort_order_string);
  $result = db_query($query);
  $discounts = array();
  while ($discount = db_fetch_object($result)) {
    $discounts[] = $discount;
  }
  return $discounts;
}

/**
 * Deletes all uc_discounts_uses rows for a discount.
 */
function uc_discounts_uses_delete_for_discount($discount_id) {
  $query = "DELETE FROM {uc_discounts_uses} WHERE discount_id=%d";
  db_query($query, $discount_id);
}

/**
 * Deletes all uc_discounts_uses rows for an order.
 */
function uc_discounts_uses_delete_for_order($order_id) {
  $query = "DELETE FROM {uc_discounts_uses} WHERE order_id=%d";
  db_query($query, $order_id);
}

/**
 * Records uses of a discount for an order
 */
function uc_discounts_uses_save_for_order($order) {
  $discounts = get_discounts_for_order($order);

  // Delete existing uses for order
  uc_discounts_uses_delete_for_order($order->order_id);

  // Insert uses (for best results use discounts returned by call to get_discounts_for_order)
  foreach ($discounts as $discount) {
    $code = !empty($discount->code) ? $discount->code : '';
    $times_applied = is_numeric($discount->times_applied) ? $discount->times_applied : 1;
    $amount = is_numeric($discount->amount) ? $discount->amount : 0;
    $discount_use = new stdClass();
    $discount_use->discount_id = $discount->discount_id;
    $discount_use->user_id = $order->uid;
    $discount_use->order_id = $order->order_id;
    $discount_use->code = $code;
    $discount_use->times_applied = $times_applied;
    $discount_use->amount = $amount;
    $discount_use->insert_timestamp = time();
    drupal_write_record('uc_discounts_uses', $discount_use);
  }
}

/**
 * Returns order codes for order.
 */
function uc_discounts_get_codes_for_order($order_id) {
  return uc_discounts_codes_to_array(db_result(db_query('SELECT codes FROM {uc_discounts_order_codes} WHERE order_id = %d', $order_id)));
}

/**
 * Applies the discounts to an order by creating the necessary line items.
 * @param  $order The order object with any codes set in the uc_discounts_code property
 * @param $compare_to_existing Compare new discounts to existing order discounts and fail if they have changed?
 * @param $save_uses Save the discounts to the uses table?  If not done here it will need to be done when the order is successfully created.
 * @return array An array with the keys: 'success' => @bool, 'message' => @str
 */
function uc_discounts_apply($order, $save_uses = TRUE, $compare_to_existing = TRUE) {

  // Store existing discount amounts
  $existing_discount_amounts = array();
  foreach (get_existing_discount_line_items($order) as $line_item) {
    $existing_discount_amounts[] = uc_currency_format($line_item["amount"]);
  }

  // Regenerate discount amounts
  $errors = array();
  $warnings = array();
  $messages = array();
  $discounts = get_discounts_for_order($order, $errors, $warnings, $messages);
  foreach ($warnings as $warning) {
    drupal_set_message($warning, 'error');
  }

  // If there were errors, print and return FALSE
  if (!empty($errors)) {
    uc_order_log_changes($order->order_id, $errors);
    foreach ($errors as $error) {
      drupal_set_message($error, 'error');
    }
    return array(
      'success' => FALSE,
      'message' => t('Discounts have changed.  Please review your cart and continue checkout.'),
    );
  }

  // Add discount line items to order
  add_discount_line_items_to_order($order, $discounts);
  $new_discount_amounts = array();
  foreach ($order->uc_discounts_line_items as $line_item) {
    $new_discount_amounts[] = uc_currency_format($line_item["amount"]);
  }
  if ($compare_to_existing) {
    $discount_intersection = array_intersect($existing_discount_amounts, $new_discount_amounts);
    if (count($discount_intersection) != count($existing_discount_amounts)) {

      // Save new discount line items
      $order->uc_discounts_line_items_need_updating = TRUE;
      uc_discounts_order('save', $order, NULL);
      return array(
        'success' => FALSE,
        'message' => t('Discounts have changed.  Please review your cart and continue checkout.'),
      );
    }
  }
  else {
    $order->uc_discounts_line_items_need_updating = TRUE;
    uc_discounts_order('save', $order, NULL);
  }
  if ($save_uses) {
    uc_discounts_uses_save_for_order($order);
  }
  uc_order_log_changes($order->order_id, $messages);
  return array(
    'success' => TRUE,
    'message' => t('Discount code(s) applied to order.'),
  );
}

/**
 * Returns existing discounts line items for order.
 */
function get_existing_discount_line_items($order) {
  if (is_array($order->line_items)) {
    $existing_line_items = $order->line_items;
  }
  else {
    $existing_line_items = uc_order_load_line_items($order, TRUE);
  }
  $line_items = array();
  foreach ($existing_line_items as $line_item) {

    //If line item type is LINE_ITEM_KEY_NAME, add it to array
    if ($line_item["type"] == LINE_ITEM_KEY_NAME) {
      $line_items[] = $line_item;
    }
  }
  return $line_items;
}

/**
 * Deletes all uc_discounts_order_codes rows for an order.
 */
function uc_discounts_order_codes_delete($order_id) {
  $query = "DELETE FROM {uc_discounts_order_codes} WHERE order_id=%d";
  db_query($query, $order_id);
}

///////////////////////////////////////////////////////////////////

//Misc. helper functions

///////////////////////////////////////////////////////////////////

/**
 * Returns a string list of codes into an array of codes
 */
function uc_discounts_codes_to_array($codes_string) {
  $codes = array();
  $raw_codes = explode("\n", $codes_string);
  foreach ($raw_codes as $raw_code) {
    $code = trim($raw_code);
    if (!empty($code)) {
      $codes[] = $code;
    }
  }
  return $codes;
}

/**
 * Create a codes string from passed codes array.
 * Note: returns "" if passed array is null
 */
function uc_discounts_codes_to_str($codes) {
  return implode("\n", (array) $codes);
}

/**
 * Returns an array of qualifying types with descriptions.
 */
function qualifying_type_options() {
  static $options = NULL;
  if (is_null($options)) {
    $options = array(
      QUALIFYING_TYPE_MINIMUM_PRICE => t("Minimum price"),
      QUALIFYING_TYPE_MINIMUM_QUANTITY => t("Minimum quantity"),
    );
  }
  return $options;
}
function qualifying_type_name($qualifying_type) {
  $options = qualifying_type_options();
  return $options[$qualifying_type];
}

/**
 * Returns an array of discount types with descriptions.
 */
function discount_type_options() {
  static $options = NULL;
  if (is_null($options)) {
    $options = array(
      DISCOUNT_TYPE_PERCENTAGE_OFF => t("Percentage off"),
      DISCOUNT_TYPE_PERCENTAGE_OFF_PER_QUALIFYING_ITEM => t("Percentage off per qualifying item"),
      DISCOUNT_TYPE_FIXED_AMOUNT_OFF => t("Fixed amount off"),
      DISCOUNT_TYPE_FIXED_AMOUNT_OFF_PER_QUALIFYING_ITEM => t("Fixed amount off per qualifying item"),
      DISCOUNT_TYPE_FREE_ITEMS => t("Free items"),
    );
  }
  return $options;
}
function discount_type_name($discount_type) {
  $options = discount_type_options();
  return $options[$discount_type];
}
function discount_amount_formatted($discount) {
  if (in_array($discount->discount_type, array(
    DISCOUNT_TYPE_PERCENTAGE_OFF,
    DISCOUNT_TYPE_PERCENTAGE_OFF_PER_QUALIFYING_ITEM,
  ))) {
    return $discount->discount_amount * 100 . '%';
  }
  elseif (in_array($discount->discount_type, array(
    DISCOUNT_TYPE_FIXED_AMOUNT_OFF,
    DISCOUNT_TYPE_FIXED_AMOUNT_OFF_PER_QUALIFYING_ITEM,
  ))) {
    return uc_currency_format($discount->discount_amount);
  }
  else {
    return $discount->discount_amount;
  }
}
function uc_discounts_add_to_existing_map_number_value(&$a, $key, $value) {
  $a[$key] = array_key_exists($key, $a) ? $a[$key] + $value : $value;
}
function uc_discounts_views_api() {
  return array(
    'api' => 2,
  );
}

Functions

Namesort descending Description
add_discount_line_items_to_order Add discount line items to order
discount_amount_formatted
discount_type_name
discount_type_options Returns an array of discount types with descriptions.
get_author_ids_for_discount Returns author_ids for discount.
get_classes_for_discount Returns Product Class names for discount.
get_codeless_discounts_for_product Returns all codeless discounts for product.
get_codeless_discounts_for_product_and_quantity Returns all codeless discounts for product when specified quantity is purchased.
get_codes_for_discount Returns codes for discount.
get_discounts_for_order Returns discounts for order. Note: $order->uc_discounts_codes must be set
get_existing_discount_line_items Returns existing discounts line items for order.
get_product_ids_for_discount Returns product_ids for discount. Note: this function does not check filter_type so a discount with filter_type other than FILTER_TYPE_PRODUCTS will return no values.
get_product_ids_for_discount_object Returns product_ids for discount object. Note: this function correctly returns all products for term-based discounts.
get_role_ids_for_discount Returns role_ids for discount.
get_skus_for_discount Returns SKUs for discount.
get_term_ids_for_discount Returns term_ids for discount.
qualifying_type_name
qualifying_type_options Returns an array of qualifying types with descriptions.
theme_uc_discounts_cart_checkout_table
uc_checkout_pane_discounts Discounts checkout pane callback
uc_discounts_add_to_cart Implementation of hook_add_to_cart()
uc_discounts_add_to_existing_map_number_value
uc_discounts_apply Applies the discounts to an order by creating the necessary line items.
uc_discounts_authors_delete Deletes all uc_discounts_authors rows for a discount.
uc_discounts_cart_pane Implementation of hook_cart_pane().
uc_discounts_cart_pane_output Adds the discounts to the cart page as line items via javascript
uc_discounts_checkout_pane Implementation of hook_checkout_pane().
uc_discounts_checkout_pane_alter Implementation of hook_checkout_pane_alter() Get discounted price to show up on checkout, & review
uc_discounts_checkout_pane_content
uc_discounts_classes_delete Deletes all uc_discounts_classes rows for a discount.
uc_discounts_codes_delete Deletes all uc_discounts_codes rows for a discount.
uc_discounts_codes_to_array Returns a string list of codes into an array of codes
uc_discounts_codes_to_str Create a codes string from passed codes array. Note: returns "" if passed array is null
uc_discounts_delete_all Deletes a uc_discounts row and all dependencies.
uc_discounts_form_uc_cart_checkout_form_alter Implementation of hook_form_FORM_ID_alter() Ensures that javascript for adding discounts is added to checkout, regardless of whether discounts pane is used.
uc_discounts_get_codes_for_order Returns order codes for order.
uc_discounts_get_discounted_price_for_product Helper function that gets the codeless discounted price of a product
uc_discounts_get_discount_amount_for_order Helper function that gets the discount amount for the order
uc_discounts_init Implementation of hook_init().
uc_discounts_js_calculate AJAX callback for discounts calculation.
uc_discounts_line_item Implementation of Ubercart's hook_line_item().
uc_discounts_load Loads a discount
uc_discounts_menu Implementation of hook_menu().
uc_discounts_order Implementation of hook_order().
uc_discounts_order_codes_delete Deletes all uc_discounts_order_codes rows for an order.
uc_discounts_order_pane Implementation of hook_order_pane
uc_discounts_order_pane_callback Callback from hook_order_pane
uc_discounts_perm Implementation of hook_perm().
uc_discounts_products_delete Deletes all uc_discounts_products rows for a discount.
uc_discounts_roles_delete Deletes all uc_discounts_roles rows for a discount.
uc_discounts_skus_delete Deletes all uc_discounts_skus rows for a discount.
uc_discounts_terms_delete Deletes all uc_discounts_terms rows for a discount.
uc_discounts_theme Implements hook_theme().
uc_discounts_uc_checkout_complete Implementation of hook_uc_checkout_complete(). Upon successful completion of checkout, record usage Note: $order->uc_discounts_codes is set by the checkout pane
uc_discounts_uses_delete_for_discount Deletes all uc_discounts_uses rows for a discount.
uc_discounts_uses_delete_for_order Deletes all uc_discounts_uses rows for an order.
uc_discounts_uses_save_for_order Records uses of a discount for an order
uc_discounts_views_api

Constants