You are here

uc_coupon.module in Ubercart Discount Coupons 5

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

Provides discount coupons for Ubercart.

Original code by Blake Lucchesi (www.boldsource.com) Maintained by David Long (dave@longwaveconsulting.com)

Send any suggestions and feedback to the above address.

File

uc_coupon.module
View source
<?php

/**
 * @file
 * Provides discount coupons for Ubercart.
 *
 * Original code by Blake Lucchesi (www.boldsource.com)
 * Maintained by David Long (dave@longwaveconsulting.com)
 *
 * Send any suggestions and feedback to the above address.
 */

/**
 * Implementation of hook_menu().
 */
function uc_coupon_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/store/customers/coupon',
      'title' => t('Coupons'),
      'description' => t('Manage store discount coupons.'),
      'callback' => 'uc_coupon_display',
      'callback arguments' => array(
        'active',
      ),
      'access' => user_access('view store coupons'),
      'type' => MENU_NORMAL_ITEM,
    );
    $items[] = array(
      'path' => 'admin/store/customers/coupon/list',
      'title' => t('Active coupons'),
      'description' => t('View active coupons.'),
      'callback' => 'uc_coupon_display',
      'callback arguments' => array(
        'active',
      ),
      'access' => user_access('view store coupons'),
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/store/customers/coupon/inactive',
      'title' => t('Inactive coupons'),
      'description' => t('View inactive coupons.'),
      'callback' => 'uc_coupon_display',
      'callback arguments' => array(
        'inactive',
      ),
      'access' => user_access('view store coupons'),
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/store/customers/coupon/add',
      'title' => t('Add new coupon'),
      'description' => t('Add a new coupon.'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'uc_coupon_add_form',
        'add',
      ),
      'access' => user_access('manage store coupons'),
      'type' => MENU_LOCAL_TASK,
      'weight' => 10,
    );
    $items[] = array(
      'path' => 'admin/store/reports/coupon',
      'title' => t('Coupon usage reports'),
      'description' => t('View coupon usage reports.'),
      'callback' => 'uc_coupon_reports',
      'access' => user_access('view reports'),
      'type' => MENU_NORMAL_ITEM,
    );
    $items[] = array(
      'path' => 'cart/checkout/coupon',
      'callback' => 'uc_coupon_checkout_apply',
      'access' => user_access('access content'),
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'uc_coupon/autocomplete/node',
      'callback' => 'uc_coupon_autocomplete_node',
      'access' => user_access('manage store coupons'),
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'uc_coupon/autocomplete/term',
      'callback' => 'uc_coupon_autocomplete_term',
      'access' => user_access('manage store coupons'),
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'uc_coupon/autocomplete/user',
      'callback' => 'uc_coupon_autocomplete_user',
      'access' => user_access('manage store coupons'),
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'uc_coupon/autocomplete/role',
      'callback' => 'uc_coupon_autocomplete_role',
      'access' => user_access('manage store coupons'),
      'type' => MENU_CALLBACK,
    );
  }
  else {
    if (arg(0) == 'admin' && arg(1) == 'store' && arg(2) == 'customers' && arg(3) == 'coupon' && is_numeric(arg(4))) {
      $items[] = array(
        'path' => 'admin/store/customers/coupon/' . arg(4) . '/edit',
        'title' => t('Edit coupon'),
        'description' => t('Edit an existing coupon.'),
        'callback' => 'drupal_get_form',
        'callback arguments' => array(
          'uc_coupon_add_form',
          'edit',
          arg(4),
        ),
        'access' => user_access('manage store coupons'),
        'type' => MENU_DYNAMIC_ITEM,
      );
      $items[] = array(
        'path' => 'admin/store/customers/coupon/' . arg(4) . '/delete',
        'title' => t('Delete coupon'),
        'description' => t('Delete a coupon.'),
        'callback' => 'drupal_get_form',
        'callback arguments' => array(
          'uc_coupon_delete_confirm',
          arg(4),
        ),
        'access' => user_access('manage store coupons'),
        'type' => MENU_DYNAMIC_ITEM,
      );
      $items[] = array(
        'path' => 'admin/store/customers/coupon/' . arg(4) . '/codes',
        'title' => t('Download bulk coupon codes'),
        'description' => t('Download the list of bulk coupon codes as a CSV file.'),
        'callback' => 'uc_coupon_codes_csv',
        'callback arguments' => array(
          arg(4),
        ),
        'access' => user_access('manage store coupons'),
        'type' => MENU_DYNAMIC_ITEM,
      );
    }
  }
  return $items;
}

/**
 * Implementation of hook_perm().
 */
function uc_coupon_perm() {
  return array(
    'view store coupons',
    'manage store coupons',
    'coupon wholesale pricing',
  );
}

/**
 * Display a brief over view of system coupons
 *
 * @param $view_type
 *   pass in an argument to filter out active/inactive coupons
 */
function uc_coupon_display($view_type = 'active') {
  _uc_coupon_paypal_check();
  $header[] = array(
    'data' => t('Name'),
    'field' => 'name',
  );
  $header[] = array(
    'data' => t('Code'),
    'field' => 'code',
    'sort' => 'asc',
  );
  $header[] = array(
    'data' => t('Value'),
    'field' => 'value',
  );
  $header[] = array(
    'data' => t('Valid from'),
    'field' => 'valid_from',
  );
  $header[] = array(
    'data' => t('Valid until'),
    'field' => 'valid_until',
  );
  $header[] = array(
    'data' => t('Actions'),
  );
  $result = pager_query('SELECT cid, name, value, code, type, valid_from, valid_until, bulk, data FROM {uc_coupons} WHERE status = %d' . tablesort_sql($header), 50, 0, NULL, $view_type == 'inactive' ? 0 : 1);
  $rows = array();
  while ($row = db_fetch_object($result)) {
    $data = unserialize($row->data);
    $name = $row->name;
    if (user_access('view all orders') && $data['order_id']) {
      $name .= ' (' . l(t('Order !id', array(
        '!id' => $data['order_id'],
      )), 'admin/store/orders/' . $data['order_id']) . ')';
    }
    if ($row->type == 'percentage') {
      $value = $row->value . '%';
    }
    else {
      $value = uc_currency_format($row->value);
    }
    $code = $row->code;
    $actions = l(t('edit'), "admin/store/customers/coupon/{$row->cid}/edit");
    if ($row->bulk) {
      $code .= '* ' . t('(bulk)');
      $actions .= ' ' . l(t('codes'), "admin/store/customers/coupon/{$row->cid}/codes");
    }
    $actions .= ' ' . l(t('delete'), "admin/store/customers/coupon/{$row->cid}/delete");
    $valid_from = format_date($row->valid_from, 'custom', variable_get('uc_date_format_default', 'm/d/Y'));
    $valid_until = format_date($row->valid_until, 'custom', variable_get('uc_date_format_default', 'm/d/Y'));
    $rows[] = array(
      $name,
      $code,
      $value,
      $valid_from,
      $valid_until,
      $actions,
    );
  }
  if (count($rows)) {
    $output = theme('table', $header, $rows, array(
      'width' => '100%',
    ));
    $output .= theme('pager', NULL, 50);
  }
  else {
    $output = '<p>' . t('There are currently no coupons in the system.') . '</p>';
  }
  return $output;
}

/**
 *  Form builder for product attributes.
 *
 * @param $action string
 * Form action, edit or add. 'edit' loads default values.
 *
 * @param $cid int
 * Coupon ID, used to load defaults when $action = 'edit'
 */
function uc_coupon_add_form($action, $cid = NULL) {
  _uc_coupon_paypal_check();
  if ($action == 'edit') {
    $value = uc_coupon_load($cid);
    $used = db_result(db_query("SELECT COUNT(*) FROM {uc_coupons_orders} WHERE cid = %d", $cid));
    $form['cid'] = array(
      '#type' => 'value',
      '#value' => $value->cid,
    );
    $form['original_code'] = array(
      '#type' => 'value',
      '#value' => $value->code,
    );
    $form['original_bulk_number'] = array(
      '#type' => 'value',
      '#value' => $value->data['bulk_number'],
    );
    $form['used'] = array(
      '#type' => 'value',
      '#value' => $used,
    );
  }
  else {
    $value->valid_from = time();
    $value->valid_until = time();
    $value->minimum_order = 0;
    $value->max_uses = 0;
    $used = 0;
  }
  $value->valid_from = array(
    'year' => format_date($value->valid_from, 'custom', 'Y'),
    'month' => format_date($value->valid_from, 'custom', 'n'),
    'day' => format_date($value->valid_from, 'custom', 'j'),
  );
  $value->valid_until = array(
    'year' => format_date($value->valid_until, 'custom', 'Y'),
    'month' => format_date($value->valid_until, 'custom', 'n'),
    'day' => format_date($value->valid_until, 'custom', 'j'),
  );
  $form['name'] = array(
    '#type' => 'textfield',
    '#title' => t('Coupon name'),
    '#default_value' => $value->name,
    '#required' => TRUE,
  );
  $form['code'] = array(
    '#type' => 'textfield',
    '#title' => t('Coupon code'),
    '#description' => t('Coupon codes cannot be changed once they have been used in an order.'),
    '#default_value' => $value->code,
    '#size' => 25,
    '#required' => !$used,
    '#maxlength' => 14,
    '#disabled' => $used,
  );
  $form['bulk'] = array(
    '#type' => 'fieldset',
    '#title' => t('Bulk coupon codes'),
    '#description' => t('The coupon code entered above will be used to prefix each generated code.'),
    '#collapsible' => TRUE,
    '#collapsed' => !$value->bulk,
  );
  if (!$used) {
    $form['bulk']['bulk_generate'] = array(
      '#type' => 'checkbox',
      '#title' => t('Enable bulk generation of coupon codes.'),
      '#default_value' => $value->bulk,
      '#disabled' => $used,
    );
  }
  else {
    $form['bulk']['bulk_generate'] = array(
      '#type' => 'value',
      '#default_value' => $value->bulk,
    );
  }
  $form['bulk']['bulk_number'] = array(
    '#type' => 'textfield',
    '#title' => t('Number of codes to generate'),
    '#default_value' => $value->data['bulk_number'],
    '#size' => 10,
    '#maxlength' => 10,
    '#disabled' => $used,
  );
  $form['bulk']['bulk_length'] = array(
    '#type' => 'select',
    '#title' => t('Code length'),
    '#description' => t('The number of characters selected here will be appended to the coupon code entered above..'),
    '#default_value' => $value->data['bulk_length'],
    '#options' => drupal_map_assoc(range(8, 30)),
    '#disabled' => $used,
  );
  $form['valid_from'] = array(
    '#type' => 'date',
    '#title' => t('Start date'),
    '#default_value' => $value->valid_from,
    '#required' => TRUE,
    '#after_build' => array(
      '_uc_coupon_date_range',
    ),
  );
  $form['valid_until'] = array(
    '#type' => 'date',
    '#title' => t('Expiry date'),
    '#default_value' => $value->valid_until,
    '#required' => TRUE,
    '#after_build' => array(
      '_uc_coupon_date_range',
    ),
  );
  $form['status'] = array(
    '#type' => 'checkbox',
    '#title' => t('Active'),
    '#description' => t('Check to enable the coupon, uncheck to disable the coupon.'),
    '#default_value' => $value->status,
  );
  $form['type'] = array(
    '#type' => 'select',
    '#title' => t('Discount type'),
    '#default_value' => $value->type,
    '#options' => array(
      'percentage' => 'Percentage',
      'price' => 'Price',
    ),
  );
  $form['value'] = array(
    '#type' => 'textfield',
    '#title' => t('Discount value'),
    '#default_value' => $value->value,
    '#size' => 10,
    '#description' => t('Enter values without symbols, for 15%, enter "15" and choose Percentage as the discount type.'),
    '#required' => TRUE,
  );
  $form['minimum_order'] = array(
    '#type' => 'textfield',
    '#title' => t('Minimum order total'),
    '#default_value' => $value->minimum_order,
    '#size' => 10,
    '#description' => t('A minimum order total that applies to the coupon, or 0 for no minimum order limit.'),
    '#required' => TRUE,
    '#field_prefix' => variable_get('uc_sign_after_amount', FALSE) ? '' : variable_get('uc_currency_sign', '$'),
    '#field_suffix' => variable_get('uc_sign_after_amount', FALSE) ? variable_get('uc_currency_sign', '$') : '',
  );
  $form['max_uses'] = array(
    '#type' => 'textfield',
    '#title' => t('Maximum number of redemptions (per code)'),
    '#default_value' => $value->max_uses,
    '#description' => t('Enter the maximum number of times each code for this coupon can be used, or 0 for unlimited.'),
    '#size' => 5,
    '#required' => TRUE,
  );
  $form['max_uses_per_user'] = array(
    '#type' => 'textfield',
    '#title' => t('Maximum number of redemptions (per user)'),
    '#default_value' => isset($value->data['max_uses_per_user']) ? $value->data['max_uses_per_user'] : 0,
    '#description' => t('Enter the maximum number of times this coupon can be used by a single user, or 0 for unlimited.'),
    '#size' => 5,
    '#required' => TRUE,
  );
  $options = array(
    '' => '(none)',
  );
  foreach (module_invoke_all('product_types') as $type) {
    $options[$type] = $type;
  }
  $form['product_types'] = array(
    '#type' => 'select',
    '#title' => t('Product classes'),
    '#description' => t('Selecting one or more product classes will restrict this coupon to matching products only. Discounts will then apply to each matching product.'),
    '#options' => $options,
    '#default_value' => $value->data['product_types'],
    '#multiple' => TRUE,
  );
  $form['products'] = array(
    '#type' => 'fieldset',
    '#title' => t('Applicable products'),
    '#description' => t('Enter one or more products below to restrict this coupon to a set of products, regardless of any product attributes. Discounts will apply to each matching product.'),
    '#tree' => TRUE,
    '#collapsible' => TRUE,
    '#collapsed' => !isset($value->data['products']),
  );
  $form['products']['negate_products'] = array(
    '#type' => 'radios',
    '#default_value' => isset($value->data['negate_products']) ? 1 : 0,
    '#options' => array(
      0 => t('Apply coupon to products listed below.'),
      1 => t('Apply coupon to all products except those listed below.'),
    ),
    '#tree' => FALSE,
  );
  if (isset($value->data['products'])) {
    foreach ($value->data['products'] as $nid) {
      $title = db_result(db_query('SELECT title FROM {node} WHERE nid = %d', $nid));
      $form['products'][] = array(
        '#type' => 'textfield',
        '#default_value' => $title . ' [nid:' . $nid . ']',
        '#autocomplete_path' => 'uc_coupon/autocomplete/node',
      );
    }
  }
  for ($i = 0; $i < 3; $i++) {
    $form['products'][] = array(
      '#type' => 'textfield',
      '#autocomplete_path' => 'uc_coupon/autocomplete/node',
    );
  }
  $form['skus'] = array(
    '#type' => 'fieldset',
    '#title' => t('Applicable SKUs'),
    '#description' => t('Enter one or more SKUs below to restrict this coupon to a set of SKUs, allowing coupons to apply to specific products or attribute options. Discounts will apply to matching SKU.'),
    '#tree' => TRUE,
    '#collapsible' => TRUE,
    '#collapsed' => !isset($value->data['skus']),
  );
  if (isset($value->data['skus'])) {
    foreach ($value->data['skus'] as $sku) {
      $form['skus'][] = array(
        '#type' => 'textfield',
        '#default_value' => $sku,
      );
    }
  }
  for ($i = 0; $i < 3; $i++) {
    $form['skus'][] = array(
      '#type' => 'textfield',
    );
  }
  $form['terms'] = array(
    '#type' => 'fieldset',
    '#title' => t('Applicable taxonomy terms'),
    '#description' => t('Enter one or more taxonomy terms (categories) below to restrict this coupon to a set of products. Discounts will apply to all matching products with these terms.'),
    '#tree' => TRUE,
    '#collapsible' => TRUE,
    '#collapsed' => !isset($value->data['terms']),
  );
  $form['terms']['negate_terms'] = array(
    '#type' => 'radios',
    '#default_value' => isset($value->data['negate_terms']) ? 1 : 0,
    '#options' => array(
      0 => t('Apply coupon to products with terms listed below.'),
      1 => t('Apply coupon to all products except those with terms listed below.'),
    ),
    '#tree' => FALSE,
  );
  if (isset($value->data['terms'])) {
    foreach ($value->data['terms'] as $tid) {
      $name = db_result(db_query('SELECT name FROM {term_data} WHERE tid = %d', $tid));
      $form['terms'][] = array(
        '#type' => 'textfield',
        '#default_value' => $name . ' [tid:' . $tid . ']',
        '#autocomplete_path' => 'uc_coupon/autocomplete/term',
      );
    }
  }
  for ($i = 0; $i < 3; $i++) {
    $form['terms'][] = array(
      '#type' => 'textfield',
      '#autocomplete_path' => 'uc_coupon/autocomplete/term',
    );
  }
  $form['users'] = array(
    '#type' => 'fieldset',
    '#title' => t('User restrictions'),
    '#description' => t('Enter one or more user names and/or "anonymous users" below to make this coupon valid only for those users.'),
    '#tree' => TRUE,
    '#collapsible' => TRUE,
    '#collapsed' => !isset($value->data['users']),
  );
  if (isset($value->data['users'])) {
    foreach ($value->data['users'] as $uid) {
      $username = $uid ? db_result(db_query('SELECT name FROM {users} WHERE uid = %d', $uid)) : t('anonymous users');
      $form['users'][] = array(
        '#type' => 'textfield',
        '#default_value' => $username . ' [uid:' . $uid . ']',
        '#autocomplete_path' => 'uc_coupon/autocomplete/user',
      );
    }
  }
  for ($i = 0; $i < 3; $i++) {
    $form['users'][] = array(
      '#type' => 'textfield',
      '#autocomplete_path' => 'uc_coupon/autocomplete/user',
    );
  }
  $form['roles'] = array(
    '#type' => 'fieldset',
    '#title' => t('Role restrictions'),
    '#description' => t('Enter one or more role names below to make this coupon valid only for users with those roles.'),
    '#tree' => TRUE,
    '#collapsible' => TRUE,
    '#collapsed' => !isset($value->data['roles']),
  );
  if (isset($value->data['roles'])) {
    foreach ($value->data['roles'] as $role) {
      $form['roles'][] = array(
        '#type' => 'textfield',
        '#default_value' => $role,
        '#autocomplete_path' => 'uc_coupon/autocomplete/role',
      );
    }
  }
  for ($i = 0; $i < 3; $i++) {
    $form['roles'][] = array(
      '#type' => 'textfield',
      '#autocomplete_path' => 'uc_coupon/autocomplete/role',
    );
  }
  $form['wholesale'] = array(
    '#type' => 'radios',
    '#title' => 'Wholesale permissions',
    '#description' => t('Select the groups who are able to use this coupon. This option is deprecated, it is recommended that you leave this option as "Both wholesale and retail" use the role selection above instead.'),
    '#default_value' => isset($value->data['wholesale']) ? $value->data['wholesale'] : 1,
    '#options' => array(
      '1' => 'Both wholesale and retail',
      '2' => 'Wholesale buyers only',
      '3' => 'Retail buyers only',
    ),
    '#required' => TRUE,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  return $form;
}
function _uc_coupon_date_range($form_element) {
  $form_element['year']['#options'] = drupal_map_assoc(range(2008, 2020));
  return $form_element;
}
function uc_coupon_autocomplete_node($string) {
  $matches = array();
  $product_types = module_invoke_all('product_types');
  $result = db_query_range("SELECT nid, title FROM {node} WHERE type IN ('" . implode("','", $product_types) . "') AND title LIKE '%%%s%'", $string, 0, 10);
  while ($row = db_fetch_object($result)) {
    $title = check_plain($row->title);
    $matches[$title . ' [nid:' . $row->nid . ']'] = $title;
  }
  print drupal_to_js($matches);
  exit;
}
function uc_coupon_autocomplete_term($string) {
  $matches = array();
  $result = db_query_range("SELECT tid, name FROM {term_data} WHERE name LIKE '%%%s%'", $string, 0, 10);
  while ($row = db_fetch_object($result)) {
    $matches[$row->name . ' [tid:' . $row->tid . ']'] = $row->name;
  }
  print drupal_to_js($matches);
  exit;
}
function uc_coupon_autocomplete_user($string) {
  $matches = array();
  $anonymous = t('anonymous users');
  if (strpos($anonymous, $string) !== FALSE) {
    $matches[$anonymous . ' [uid:0]'] = $anonymous;
  }
  $result = db_query_range("SELECT uid, name FROM {users} WHERE name LIKE '%%%s%'", $string, 0, 10);
  while ($row = db_fetch_object($result)) {
    $matches[$row->name . ' [uid:' . $row->uid . ']'] = $row->name;
  }
  print drupal_to_js($matches);
  exit;
}
function uc_coupon_autocomplete_role($string) {
  $matches = array();
  $result = db_query_range("SELECT name FROM {role} WHERE name LIKE '%%%s%'", $string, 0, 10);
  while ($row = db_fetch_object($result)) {
    $matches[$row->name] = $row->name;
  }
  print drupal_to_js($matches);
  exit;
}

/**
 * Coupon form validate handler.
 */
function uc_coupon_add_form_validate($form_id, $form) {

  // check to ensure a unique coupon code
  $name = db_result(db_query("SELECT name FROM {uc_coupons} WHERE code = '%s' AND cid <> %d", strtoupper($form['code']), $form['cid']));
  if ($name) {
    form_set_error('code', t('Coupon code already used by %name.', array(
      '%name' => $name,
    )));
  }
  foreach ($form['products'] as $key => $product) {
    if ($product && !preg_match('/\\[nid:(\\d+)\\]$/', $product)) {
      form_set_error('products][' . $key, t('Products must include the node ID.'));
    }
  }
  foreach ($form['users'] as $key => $user) {
    if ($user && !preg_match('/\\[uid:(\\d+)\\]$/', $user)) {
      form_set_error('users][' . $key, t('User names must include the user ID.'));
    }
  }
  if (!$form['used'] && $form['bulk_generate'] && intval($form['bulk_number']) <= 0) {
    form_set_error('bulk_number', t('You must specify the number of codes to generate.'));
  }
  $valid_from = mktime(0, 0, 0, $form['valid_from']['month'], $form['valid_from']['day'], $form['valid_from']['year']);
  $valid_until = mktime(0, 0, 0, $form['valid_until']['month'], $form['valid_until']['day'], $form['valid_until']['year']);
  if ($valid_from > $valid_until) {
    form_set_error('valid_from', t('The coupon start date must be before the expiration date.'));
  }
}

/**
 * Coupon form submit handler.
 */
function uc_coupon_add_form_submit($form_id, $form) {

  // If the coupon was previously used, reset disabled textfields to their original values.
  if ($form['used']) {
    $form['code'] = $form['original_code'];
    $form['bulk_number'] = $form['original_bulk_number'];
  }
  $code = strtoupper($form['code']);
  $valid_from = mktime(0, 0, 0, $form['valid_from']['month'], $form['valid_from']['day'], $form['valid_from']['year']);
  $valid_until = mktime(0, 0, 0, $form['valid_until']['month'], $form['valid_until']['day'], $form['valid_until']['year']);
  $data = array();
  if ($form['bulk_generate']) {
    $data['bulk_number'] = $form['bulk_number'];
    $data['bulk_length'] = $form['bulk_length'];
  }
  if ($form['max_uses_per_user']) {
    $data['max_uses_per_user'] = $form['max_uses_per_user'];
  }
  if ($form['negate_products']) {
    $data['negate_products'] = TRUE;
  }
  if ($form['negate_terms']) {
    $data['negate_terms'] = TRUE;
  }
  foreach ($form['product_types'] as $type) {
    if ($type) {
      $data['product_types'][] = $type;
    }
  }
  foreach ($form['products'] as $key => $product) {
    if ($product && preg_match('/\\[nid:(\\d+)\\]$/', $product, $matches)) {
      $data['products'][] = $matches[1];
    }
  }
  foreach ($form['skus'] as $sku) {
    if ($sku) {
      $data['skus'][] = $sku;
    }
  }
  foreach ($form['terms'] as $key => $term) {
    if ($term && preg_match('/\\[tid:(\\d+)\\]$/', $term, $matches)) {
      $data['terms'][] = $matches[1];
    }
  }
  foreach ($form['users'] as $key => $user) {
    if ($user && preg_match('/\\[uid:(\\d+)\\]$/', $user, $matches)) {
      $data['users'][] = $matches[1];
    }
  }
  foreach ($form['roles'] as $role) {
    if ($role) {
      $data['roles'][] = $role;
    }
  }
  $data['wholesale'] = $form['wholesale'];

  // If the forms coupon id is not set then we try to insert a new coupon
  if (!isset($form['cid'])) {

    // Only set bulk coupon seed once.
    db_query("INSERT INTO {uc_coupons} (name, code, value, type, status, valid_from, valid_until, max_uses, minimum_order, data, bulk, bulk_seed) VALUES ('%s', '%s', %f, '%s', %d, %d, %d, %d, %f, '%s', %d, '%s')", $form['name'], $code, $form['value'], $form['type'], $form['status'], $valid_from, $valid_until, $form['max_uses'], $form['minimum_order'], serialize($data), $form['bulk_generate'], md5(uniqid()));
    drupal_set_message(t('Coupon %name has been created.', array(
      '%name' => $form['name'],
    )));
  }
  else {
    db_query("UPDATE {uc_coupons} SET name = '%s', code = '%s', value = %f, type = '%s', status = %d, valid_from = %d, valid_until = %d, max_uses = %d, minimum_order = %f, data = '%s', bulk = %d WHERE cid = %d", $form['name'], $code, $form['value'], $form['type'], $form['status'], $valid_from, $valid_until, $form['max_uses'], $form['minimum_order'], serialize($data), $form['bulk_generate'], $form['cid']);
    drupal_set_message(t('Coupon %name has been updated.', array(
      '%name' => $form['name'],
    )));
  }
  drupal_goto('admin/store/customers/coupon' . ($form['status'] ? '' : '/inactive'));
}

/**
 * Load a coupon into the form for editing
 *
 * @param $cid
 *  Unique Coupin ID.
 *
 * @return $coupon
 *  Returns a coupon object.
 */
function uc_coupon_load($cid) {
  $coupon = db_fetch_object(db_query("SELECT * FROM {uc_coupons} WHERE cid = %d", $cid));
  if (!$coupon) {
    drupal_not_found();
    return;
  }
  if ($coupon->data) {
    $coupon->data = unserialize($coupon->data);
  }
  return $coupon;
}

/**
 * Delete coupon confirm form
 *
 * @param $cid int
 * Coupon ID.
 *
 * @return $confirm
 *  Return a drupal confirm form.
 */
function uc_coupon_delete_confirm($cid) {
  $coupon = uc_coupon_load($cid);
  $form['cid'] = array(
    '#type' => 'value',
    '#value' => $cid,
  );
  return confirm_form($form, t('Are you sure you want to delete coupon %name with code %code?', array(
    '%name' => $coupon->name,
    '%code' => $coupon->code,
  )), 'admin/store/customers/coupon', t('This action cannot be undone. Deleting this coupon will remove all records of past uses as well.'), t('Delete'));
}
function uc_coupon_delete_confirm_submit($form_id, $form) {
  $coupon = uc_coupon_load($form['cid']);
  db_query("DELETE FROM {uc_coupons} WHERE cid = %d", $form['cid']);
  db_query("DELETE FROM {uc_coupons_orders} WHERE cid = %d", $form['cid']);
  drupal_set_message(t('Coupon %name has been deleted.', array(
    '%name' => $coupon->name,
  )));
  drupal_goto('admin/store/customers/coupon' . ($coupon->status ? '' : '/inactive'));
}

/**
 * Generate a list of bulk coupon codes.
 */
function uc_coupon_codes_csv($cid) {
  $coupon = uc_coupon_load($cid);
  if (!$coupon->bulk) {
    drupal_not_found();
    return;
  }
  header('Content-Type: application/octet-stream');
  header('Content-Disposition: attachment; filename="' . $coupon->code . '.csv";');
  for ($id = 0; $id < $coupon->data['bulk_number']; $id++) {
    echo uc_coupon_get_bulk_code($coupon, $id) . "\n";
  }
  exit;
}

/**
 * Generate a single bulk coupon code.
 */
function uc_coupon_get_bulk_code($coupon, $id) {
  $id = str_pad(dechex($id), strlen(dechex($coupon->data['bulk_number'])), '0', STR_PAD_LEFT);
  $length = strlen($coupon->code) + $coupon->data['bulk_length'];
  return strtoupper(substr($coupon->code . $id . md5($coupon->bulk_seed . $id), 0, $length));
}

/**
 * Load a coupon (single or bulk) from the supplied code.
 */
function uc_coupon_find($code) {

  // Look for matching single coupon first.
  $coupon = db_fetch_object(db_query("SELECT * FROM {uc_coupons} WHERE code = '%s' AND status = 1 AND bulk = 0 AND valid_from < %d AND valid_until > %d", $code, time(), time()));
  if ($coupon !== FALSE) {
    if ($coupon->data) {
      $coupon->data = unserialize($coupon->data);
    }
    return $coupon;
  }

  // Look through bulk coupons.
  $result = db_query("SELECT * FROM {uc_coupons} WHERE status = 1 AND bulk = 1 AND valid_from < %d AND valid_until > %d", time(), time());
  while ($coupon = db_fetch_object($result)) {

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

/**
 * Validate a coupon and calculate the coupon amount against the current cart contents.
 *
 * @param $code
 *  The coupon code entered at the checkout screen
 *
 * @return
 *  Returns a coupon result object with details about the validation
 */
function uc_coupon_validate($code) {
  global $user;
  $result->valid = FALSE;
  $code = strtoupper($code);
  $coupon = uc_coupon_find($code);
  if (!$coupon) {
    $result->message = t('This coupon code is invalid or has expired.');
    return $result;
  }
  if (isset($coupon->data['products']) || isset($coupon->data['skus']) || isset($coupon->data['terms']) || isset($coupon->data['product_types'])) {

    // Product coupons apply to the subtotal and quantity of matching products.
    foreach (uc_cart_get_contents() as $item) {
      $cart_total += $item->price * $item->qty;
      $terms = array();
      $query = db_query("SELECT tid FROM {term_node} WHERE nid = %d", $item->nid);
      while ($row = db_fetch_object($query)) {
        $terms[] = $row->tid;
      }
      if (isset($coupon->data['products']) && (isset($coupon->data['negate_products']) xor in_array($item->nid, $coupon->data['products']))) {
        $applicable_total += $item->price * $item->qty;
        $applicable_qty += $item->qty;
      }
      else {
        if (isset($coupon->data['skus']) && in_array($item->model, $coupon->data['skus'])) {
          $applicable_total += $item->price * $item->qty;
          $applicable_qty += $item->qty;
        }
        else {
          if (isset($coupon->data['terms']) && (isset($coupon->data['negate_terms']) xor count(array_intersect($terms, $coupon->data['terms'])))) {
            $applicable_total += $item->price * $item->qty;
            $applicable_qty += $item->qty;
          }
          else {
            if (isset($coupon->data['product_types'])) {
              $type = db_result(db_query("SELECT type FROM {node} WHERE nid = %d", $item->nid));
              if (in_array($type, $coupon->data['product_types'])) {
                $applicable_total += $item->price * $item->qty;
                $applicable_qty += $item->qty;
              }
            }
          }
        }
      }
    }
  }
  else {

    // Standard coupons apply once to the whole cart.
    foreach (uc_cart_get_contents() as $item) {
      $cart_total += $item->price * $item->qty;
    }
    $applicable_total = $cart_total;
    $applicable_qty = 1;
  }
  if ($applicable_total == 0) {
    $result->message = t('You do not have applicable products in your cart.');
    return $result;
  }

  //  CHECK MAX USES
  if ($coupon->max_uses > 0) {
    $used = db_result(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 > 0 AND uco.cid = %d AND uco.code = '%s'", $coupon->cid, $code));
    if ($used >= $coupon->max_uses) {
      $result->message = t('This coupon has reached the maximum redemption limit.');
      return $result;
    }
  }

  //  CHECK MAX USES PER USER
  if (isset($coupon->data['max_uses_per_user'])) {
    $used = db_result(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 > 0 AND uco.cid = %d AND uo.uid = %d", $coupon->cid, $user->uid));
    if ($used >= $coupon->data['max_uses_per_user']) {
      $result->message = t('This coupon has reached the maximum redemption limit.');
      return $result;
    }
  }

  //  CHECK MINIMUM PURCHASE VALUE
  if ($coupon->minimum_order > 0 && $coupon->minimum_order > $cart_total) {
    $result->message = t('You have not reached the minimum order total for this coupon.');
    return $result;
  }

  //  CHECK USER ID
  if (isset($coupon->data['users'])) {
    if (!in_array("{$user->uid}", $coupon->data['users'], TRUE)) {
      $result->message = t('Your user ID is not allowed to use this coupon.');
      return $result;
    }
  }

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

  // CHECK USER PERMISSIONS
  // 1 - both wholesale and retail any user
  // 2 - wholesale only -> users with 'coupon wholesale pricing'
  // 3 - retail only -> users without 'coupon wholesale pricing'
  if ($coupon->data['wholesale'] > 1) {
    if ($coupon->data['wholesale'] == 2) {
      if (!user_access('coupon wholesale pricing')) {
        $result->message = t('You do not have the correct permission to use this coupon.');
        return $result;
      }
    }
    else {
      if ($coupon->data['wholesale'] == 3) {
        if (user_access('coupon wholesale pricing')) {
          $result->message = t('You do not have the correct permission to use this coupon.');
          return $result;
        }
      }
    }
  }
  $result->valid = TRUE;
  $result->code = $coupon->code;
  $result->cid = $coupon->cid;
  $result->title = t('Coupon: @code', array(
    '@code' => $coupon->code,
  ));
  if ($coupon->type == 'percentage') {
    $result->amount = $applicable_total * $coupon->value / 100;
  }
  else {
    if ($coupon->type == 'price') {
      $result->amount = min($applicable_total, $applicable_qty * $coupon->value);
    }
  }
  return $result;
}

/**
 * Implementation of hook_checkout_pane().
 *
 * Show a pane just above the order total that allows shoppers to enter a coupon
 * for a discount.
 */
function uc_coupon_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,
  );
  return $panes;
}

/**
 * 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, &$arg1, $arg2) {
  switch ($op) {
    case 'view':
      uc_add_js(drupal_get_path('module', 'uc_coupon') . '/uc_coupon.js');
      if ($_SESSION['uc_coupon']) {
        $code = $_SESSION['uc_coupon'];
        unset($_SESSION['uc_coupon']);
      }
      else {
        $code = $arg1->data['coupon'];
      }
      $coupon = uc_coupon_validate($code);
      if ($coupon->valid) {
        $settings = array(
          'title' => $coupon->title,
          'amount' => $coupon->amount,
        );
        uc_add_js(array(
          'uc_coupon' => $settings,
        ), 'setting');
      }
      $description = variable_get('uc_coupon_pane_description', 'Enter a coupon code for this order.');
      $contents['code'] = array(
        '#type' => 'textfield',
        '#title' => t('Coupon code'),
        '#default_value' => $code,
        '#size' => 25,
      );
      $contents['apply'] = array(
        '#type' => 'button',
        '#value' => t('Apply to order'),
        '#attributes' => array(
          'onclick' => "getCoupon(); return false;",
        ),
      );
      return array(
        'description' => $description,
        'contents' => $contents,
      );
    case 'process':
      if ($arg2['code']) {
        $code = strtoupper(check_plain($arg2['code']));
        $arg1->data['coupon'] = $code;
        $coupon = uc_coupon_validate($code);
        if (!$coupon->valid) {
          drupal_set_message($coupon->message, 'error');
          return FALSE;
        }
        $result = db_query("SELECT line_item_id FROM {uc_order_line_items} WHERE order_id = %d AND type = 'coupon'", $arg1->order_id);
        if ($lid = db_result($result)) {
          db_query("UPDATE {uc_coupons_orders} SET cid = %d, code = '%s', value = %f WHERE oid = %d", $coupon->cid, $code, $coupon->amount, $arg1->order_id);
          uc_order_update_line_item($lid, $coupon->title, -$coupon->amount);
        }
        else {
          db_query("INSERT INTO {uc_coupons_orders} (cid, oid, code, value) VALUES (%d, %d, '%s', %f)", $coupon->cid, $arg1->order_id, $code, $coupon->amount);
          uc_order_line_item_add($arg1->order_id, 'coupon', $coupon->title, -$coupon->amount);
        }
      }
      return TRUE;
    case 'settings':
      $form['uc_coupon_pane_description'] = array(
        '#type' => 'textarea',
        '#title' => t('Checkout pane message'),
        '#default_value' => variable_get('uc_coupon_pane_description', 'Enter a coupon code for this order.'),
      );
      return $form;
  }
}

/**
 * Checkout pane AJAX callback.
 **/
function uc_coupon_checkout_apply() {
  $coupon = uc_coupon_validate($_POST['code']);
  if ($coupon->valid) {
    $_SESSION['uc_coupon'] = $_POST['code'];
    $coupon->message = t('The coupon has been applied to your order.');
  }
  drupal_set_header("Content-Type: text/javascript; charset=utf-8");
  print drupal_to_js($coupon);
  exit;
}

/**
 * Implementation of hook_line_item().
 */
function uc_coupon_line_item() {
  $items[] = array(
    'id' => 'coupon',
    'title' => t('Coupon discount'),
    'weight' => 2,
    'default' => FALSE,
    'stored' => TRUE,
    'add_list' => TRUE,
    'calculated' => TRUE,
  );
  return $items;
}

/**
 * Coupon report form.
 */
function uc_coupon_reports_form($start = NULL, $end = NULL, $statuses = NULL) {
  if (is_null($start)) {
    $start = time();
  }
  if (is_null($end)) {
    $end = time();
  }
  if (is_null($statuses)) {
    $statuses = variable_get('uc_reports_reported_statuses', array(
      'completed',
    ));
  }
  $options = array();
  foreach (uc_order_status_list() as $status) {
    $options[$status['id']] = $status['title'];
  }
  $form['start'] = array(
    '#type' => 'date',
    '#title' => t('Start date'),
    '#default_value' => array(
      'year' => format_date($start, 'custom', 'Y'),
      'month' => format_date($start, 'custom', 'n'),
      'day' => format_date($start, 'custom', 'j'),
    ),
    '#after_build' => array(
      '_uc_coupon_date_range',
    ),
  );
  $form['end'] = array(
    '#type' => 'date',
    '#title' => t('End date'),
    '#default_value' => array(
      'year' => format_date($end, 'custom', 'Y'),
      'month' => format_date($end, 'custom', 'n'),
      'day' => format_date($end, 'custom', 'j'),
    ),
    '#after_build' => array(
      '_uc_coupon_date_range',
    ),
  );
  $form['status'] = array(
    '#type' => 'select',
    '#title' => t('Order statuses'),
    '#description' => t('Only orders with selected statuses will be included in the report.') . '<br />' . t('Hold Ctrl + click to select multiple statuses.'),
    '#options' => $options,
    '#default_value' => $statuses,
    '#multiple' => TRUE,
    '#size' => 5,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Display report'),
  );
  return $form;
}

/**
 * Handle form submit and assign variables
 */
function uc_coupon_reports_form_submit($form_id, $form_values) {
  $start = mktime(0, 0, 0, $form_values['start']['month'], $form_values['start']['day'], $form_values['start']['year']);
  $end = mktime(23, 59, 59, $form_values['end']['month'], $form_values['end']['day'], $form_values['end']['year']);
  $statuses = implode(',', array_keys($form_values['status']));
  drupal_goto('admin/store/reports/coupon/' . $start . '/' . $end . '/' . $statuses);
}

/**
 * Output Coupon Reports
 *
 * TODO: Integrate with UberCart reports functionality.
 */
function uc_coupon_reports($start = NULL, $end = NULL, $statuses = NULL) {
  drupal_add_css(drupal_get_path('module', 'uc_coupon') . '/reports.css', 'uc_coupon');
  if (is_null($statuses)) {
    $statuses = variable_get('uc_reports_reported_statuses', array(
      'completed',
    ));
  }
  else {
    $statuses = explode(',', $statuses);
  }
  $output = drupal_get_form('uc_coupon_reports_form', $start, $end, $statuses);
  if (isset($start) && isset($end)) {
    $query = db_query("SELECT co.cid, co.oid, co.value, co.code, o.order_total, o.created FROM {uc_coupons_orders} AS co LEFT JOIN {uc_orders} AS o ON (co.oid = o.order_id) WHERE o.created > %d AND o.created < %d AND o.order_status IN ('" . implode("', '", $statuses) . "') ORDER BY co.cid, o.created ASC", $start, $end);
    $row_header = array(
      t('Order #'),
      t('Purchase date'),
      t('Total'),
      t('Coupon value'),
    );
    $last_cid = 0;
    while ($row = db_fetch_object($query)) {

      // Display the table of coupons if this is the next set of coupons
      if ($row->cid != $last_cid and $last_cid != 0) {
        $td[] = array(
          '',
          '<b>' . t('Uses: @total', array(
            '@total' => $num_uses,
          )) . '</b>',
          '<b>' . uc_currency_format($coupon_sale_amount) . '</b>',
          '<b>' . uc_currency_format($coupon_amount) . '</b>',
        );
        $data .= theme('table', $row_header, $td, array(
          'width' => '100%',
        ));
        $td = array();
        $num_uses = 0;
        $coupon_amount = 0;
        $coupon_sale_amount = 0;
      }

      // if this is the first coupon of the set display the header first
      if ($row->cid != $last_cid || ($last_cid = 0)) {
        $data .= '<div class="totals">' . t('Coupon code: !link', array(
          '!link' => l($row->code, 'admin/store/customers/coupon/' . $row->cid . '/edit'),
        )) . '</div>';
      }
      $td[] = array(
        l('#' . $row->oid, 'admin/store/orders/' . $row->oid),
        format_date($row->created, 'custom', variable_get('uc_date_format_default', 'm/d/Y')),
        uc_currency_format($row->order_total),
        uc_currency_format($row->value),
      );
      $num_uses++;
      $coupon_amount += $row->value;
      $coupon_sale_amount += $row->order_total;
      $last_cid = $row->cid;
      $orders_total += $row->order_total;
      $coupons_total += $row->value;
    }
    $td[] = array(
      '',
      '<b>' . t('Uses: @total', array(
        '@total' => $num_uses,
      )) . '</b>',
      '<b>' . uc_currency_format($coupon_sale_amount) . '</b>',
      '<b>' . uc_currency_format($coupon_amount) . '</b>',
    );
    $data .= theme('table', $row_header, $td, array(
      'width' => '100%',
    ));
    $output .= '<h2>' . t('Coupon usage report') . '</h2>';
    $output .= $data;
    $output .= '<br /><table width="100%"><tr>';
    $output .= '<td>' . t('Coupons used: @total', array(
      '@total' => db_num_rows($query),
    )) . '</td>';
    $output .= '<td>' . t('Orders total: @total', array(
      '@total' => uc_currency_format($orders_total),
    )) . '</td>';
    $output .= '<td>' . t('Coupons total: @total', array(
      '@total' => uc_currency_format($coupons_total),
    )) . '</td>';
    $output .= '</tr></table>';
  }
  return $output;
}

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

/**
 * Implementation of hook_store_status().
 */
function uc_coupon_store_status() {
  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;
}

Functions

Namesort descending Description
uc_checkout_pane_coupon Checkout Pane callback function.
uc_coupon_add_form Form builder for product attributes.
uc_coupon_add_form_submit Coupon form submit handler.
uc_coupon_add_form_validate Coupon form validate handler.
uc_coupon_autocomplete_node
uc_coupon_autocomplete_role
uc_coupon_autocomplete_term
uc_coupon_autocomplete_user
uc_coupon_checkout_apply Checkout pane AJAX callback.
uc_coupon_checkout_pane Implementation of hook_checkout_pane().
uc_coupon_codes_csv Generate a list of bulk coupon codes.
uc_coupon_delete_confirm Delete coupon confirm form
uc_coupon_delete_confirm_submit
uc_coupon_display Display a brief over view of system coupons
uc_coupon_find Load a coupon (single or bulk) from the supplied code.
uc_coupon_get_bulk_code Generate a single bulk coupon code.
uc_coupon_line_item Implementation of hook_line_item().
uc_coupon_load Load a coupon into the form for editing
uc_coupon_menu Implementation of hook_menu().
uc_coupon_perm Implementation of hook_perm().
uc_coupon_reports Output Coupon Reports
uc_coupon_reports_form Coupon report form.
uc_coupon_reports_form_submit Handle form submit and assign variables
uc_coupon_store_status Implementation of hook_store_status().
uc_coupon_validate Validate a coupon and calculate the coupon amount against the current cart contents.
_uc_coupon_date_range
_uc_coupon_paypal_check Show a message if PayPal is enabled and "itemized order" is selected.