'Coupons', 'description' => 'Manage store discount coupons.', 'page callback' => 'uc_coupon_display', 'page arguments' => array('active'), 'access arguments' => array('view store coupons'), 'type' => MENU_NORMAL_ITEM, 'file' => 'uc_coupon.admin.inc', ); $items['admin/store/coupons/list'] = array( 'title' => 'Active coupons', 'description' => 'View active coupons.', 'page callback' => 'uc_coupon_display', 'page arguments' => array('active'), 'access arguments' => array('view store coupons'), 'type' => MENU_NORMAL_ITEM, 'file' => 'uc_coupon.admin.inc', 'weight' => 0, ); $items['admin/store/coupons/inactive'] = array( 'title' => 'Inactive coupons', 'description' => 'View inactive coupons.', 'page callback' => 'uc_coupon_display', 'page arguments' => array('inactive'), 'access arguments' => array('view store coupons'), 'type' => MENU_NORMAL_ITEM, 'file' => 'uc_coupon.admin.inc', 'weight' => 1, ); $items['admin/store/coupons/add'] = array( 'title' => 'Add new coupon', 'description' => 'Add a new coupon.', 'page callback' => 'drupal_get_form', 'page arguments' => array('uc_coupon_add_form'), 'access arguments' => array('manage store coupons'), 'type' => MENU_NORMAL_ITEM, 'file' => 'uc_coupon.admin.inc', 'weight' => 2, ); $items['admin/store/coupons/%uc_coupon'] = array( 'title callback' => 'uc_coupon_title', 'title arguments' => array(3), 'description' => 'View coupon details.', 'page callback' => 'uc_coupon_view', 'page arguments' => array(3), 'access arguments' => array('view store coupons'), 'type' => MENU_CALLBACK, 'file' => 'uc_coupon.admin.inc', 'weight' => 3, ); $items['admin/store/coupons/%uc_coupon/view'] = array( 'title' => 'View', 'description' => 'View coupon details.', 'access arguments' => array('view store coupons'), 'type' => MENU_DEFAULT_LOCAL_TASK, 'file' => 'uc_coupon.admin.inc', 'weight' => 0, ); $items['admin/store/coupons/%uc_coupon/print'] = array( 'title' => 'Print', 'description' => 'Print coupon.', 'page callback' => 'uc_coupon_print', 'page arguments' => array(3, 5, 'print'), 'access arguments' => array('view store coupons'), 'type' => MENU_LOCAL_TASK, 'file' => 'uc_coupon.admin.inc', 'weight' => 1, ); $items['admin/store/coupons/%uc_coupon/edit'] = array( 'title' => 'Edit', 'description' => 'Edit an existing coupon.', 'page callback' => 'drupal_get_form', 'page arguments' => array('uc_coupon_add_form', 3), 'access arguments' => array('manage store coupons'), 'type' => MENU_LOCAL_TASK, 'file' => 'uc_coupon.admin.inc', 'weight' => 2, ); $items['admin/store/coupons/%uc_coupon/delete'] = array( 'title' => 'Delete', 'description' => 'Delete a coupon.', 'page callback' => 'drupal_get_form', 'page arguments' => array('uc_coupon_delete_confirm', 3), 'access arguments' => array('manage store coupons'), 'type' => MENU_LOCAL_TASK, 'file' => 'uc_coupon.admin.inc', 'weight' => 3, ); $items['admin/store/coupons/%uc_coupon/codes'] = array( 'title' => 'Download bulk coupon codes', 'description' => 'Download the list of bulk coupon codes as a CSV file.', 'page callback' => 'uc_coupon_codes_csv', 'page arguments' => array(3), 'access arguments' => array('view store coupons'), 'file' => 'uc_coupon.admin.inc', 'type' => MENU_CALLBACK, ); $items['admin/store/coupons/autocomplete/node'] = array( 'title' => 'Node autocomplete', 'page callback' => 'uc_coupon_autocomplete_node', 'access arguments' => array('manage store coupons'), 'type' => MENU_CALLBACK, 'file' => 'uc_coupon.admin.inc', ); $items['admin/store/coupons/autocomplete/term'] = array( 'title' => 'Term autocomplete', 'page callback' => 'uc_coupon_autocomplete_term', 'access arguments' => array('manage store coupons'), 'type' => MENU_CALLBACK, 'file' => 'uc_coupon.admin.inc', ); $items['admin/store/coupons/autocomplete/user'] = array( 'title' => 'User autocomplete', 'page callback' => 'uc_coupon_autocomplete_user', 'access arguments' => array('manage store coupons'), 'type' => MENU_CALLBACK, 'file' => 'uc_coupon.admin.inc', ); $items['admin/store/coupons/autocomplete/role'] = array( 'title' => 'Role autocomplete', 'page callback' => 'uc_coupon_autocomplete_role', 'access arguments' => array('manage store coupons'), 'type' => MENU_CALLBACK, 'file' => 'uc_coupon.admin.inc', ); $items['admin/store/settings/coupon'] = array( 'title' => 'Coupon module settings', 'description' => 'Configure the discount coupon module settings.', 'page callback' => 'drupal_get_form', 'page arguments' => array('uc_coupon_settings_form'), 'access arguments' => array('administer store'), 'file' => 'uc_coupon.admin.inc', 'type' => MENU_NORMAL_ITEM, ); $items['admin/store/settings/coupon/settings'] = array( 'title' => 'Settings', 'description' => 'Edit the basic coupon settings.', 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10, ); $items['admin/store/reports/coupon'] = array( 'title' => 'Coupon usage reports', 'description' => 'View coupon usage reports.', 'page callback' => 'uc_coupon_reports', 'access arguments' => array('view reports'), 'file' => 'uc_coupon.reports.inc', 'type' => MENU_NORMAL_ITEM, ); return $items; } /** * Properly handle %uc_coupon wildcard. * (Necessary to prevent PHP runtime notice.) */ function uc_coupon_to_arg($arg) { return $arg; } /** * Title callback for coupon print preview. */ function uc_coupon_title($coupon) { return $coupon->name; } /** * Implements hook_permission(). */ function uc_coupon_permission() { $perms = array( 'view store coupons' => array( 'title' => t('view store coupons'), 'description' => t('Display information about discount coupons.'), ), 'manage store coupons' => array( 'title' => t('manage store coupons'), 'description' => t('Create, edit and delete discoutn coupons.'), ), ); if (!module_exists('uc_reports')) { $perms['view reports'] = array( 'title' => t('view reports'), 'description' => t('Display coupon usage reports.') ); } return $perms; } /** * Implements hook_init(). */ function uc_coupon_init() { global $conf; $conf['i18n_variables'][] = 'uc_coupon_pane_description'; // Auto apply coupon from query string, if configured. if ($param = variable_get('uc_coupon_querystring', '')) { if (isset($_GET[$param]) && $_GET[$param]) { // We retain the querystring coupon so that it will validate if/when appropriate conditions are met. uc_coupon_session_add($_GET[$param], 'retain'); } } } /** * Implements hook_theme(). */ function uc_coupon_theme() { return array( 'uc_coupon_automatic_discounts' => array( 'render element' => 'form', ), 'uc_coupon_form' => array( 'render element' => 'form', ), 'uc_coupon_actions' => array( 'variables' => array('coupon' => NULL), 'file' => 'uc_coupon.admin.inc', ), 'uc_coupon_code' => array( 'variables' => array('coupon' => NULL), 'file' => 'uc_coupon.admin.inc', ), 'uc_coupon_discount' => array( 'variables' => array('coupon' => NULL, 'currency' => TRUE), ), 'uc_coupon_certificate' => array( 'variables' => array('coupon' => NULL, 'code' => NULL), 'template' => 'uc-coupon-certificate', 'path' => drupal_get_path('module', 'uc_coupon') . '/theme', ), 'uc_coupon_page' => array( 'variables' => array('content' => NULL), 'template' => 'uc-coupon-page', 'path' => drupal_get_path('module', 'uc_coupon') . '/theme', ), ); } /** * Implements hook_theme_registry_alter(). */ function uc_coupon_theme_registry_alter(&$registry) { // Override the default theme for the cart block content - but only if not already overridden. if ($registry['uc_cart_block_content']['function'] == 'theme_uc_cart_block_content') { $registry['uc_cart_block_content']['function'] = 'uc_coupon_theme_uc_cart_block_content'; } } /** * Count usage of a coupon. * * @param $cid * The coupon id to count. * @param $uid * (optional) The user id to count. Defaults to the current user. * @param array $exclude_oids * (optional) If supplied, will exclude usage for the specified order ids. * * @return * An associative array containing: * - codes: An associative array of code => usage count. * - user: The usage count by the specified (or current) user. */ function uc_coupon_count_usage($cid, $uid = NULL, $exclude_oids = array()) { global $user; $weight = uc_order_status_data(variable_get('uc_coupon_used_order_status', 'processing'), 'weight'); $usage = array('codes' => array(), 'value' => array('codes' => array())); $exclude_where = empty($exclude_oids) ? '' : 'AND uo.order_id NOT IN (:oids)'; $result = db_query("SELECT uco.code, COUNT(*) AS uses, SUM(uco.value) AS value FROM {uc_coupons_orders} AS uco LEFT JOIN {uc_orders} AS uo ON uco.oid = uo.order_id LEFT JOIN {uc_order_statuses} AS uos ON uo.order_status = uos.order_status_id WHERE uos.weight >= :weight AND uco.cid = :cid $exclude_where GROUP BY uco.code", array( ':weight' => $weight, ':cid' => $cid, ':oids' => $exclude_oids)); foreach ($result as $row) { $usage['codes'][$row->code] = $row->uses; $usage['value']['codes'][$row->code] = $row->value; } if (is_null($uid)) { $uid = $user->uid; } $usage['user'] = db_query("SELECT COUNT(*) FROM {uc_coupons_orders} AS uco LEFT JOIN {uc_orders} AS uo ON uco.oid = uo.order_id LEFT JOIN {uc_order_statuses} AS uos ON uo.order_status = uos.order_status_id WHERE uos.weight >= :weight AND uco.cid = :cid AND uo.uid = :uid", array( ':weight' => $weight, ':cid' => $cid, ':uid' => $uid))->fetchField(); // Allow other modules to implement usage counts. drupal_alter('uc_coupon_usage', $usage, $cid, $uid); return $usage; } /** * Theme for a coupon discount. * @param $variables * 'coupon' => The coupon whose discount is to be themed. * 'currency' => TRUE to include currency symbols. */ function theme_uc_coupon_discount($variables) { $coupon = $variables['coupon']; $currency = isset($variables['currency']) ? $variables['currency'] : TRUE; return _uc_coupon_format_discount($coupon, $currency); } /** * Format a coupon's value depending on the type, optionally including currency symbols. */ function _uc_coupon_format_discount($coupon, $currency = TRUE) { switch ($coupon->type) { case 'price': case 'credit': return $currency ? uc_currency_format($coupon->value) : $coupon->value; case 'percentage': return (float) $coupon->value . '%'; case 'set_price': return '=' . ($currency ? uc_currency_format($coupon->value) : $coupon->value); } } /** * Generate a single bulk coupon code. */ function uc_coupon_get_bulk_code($coupon, $id) { // If this coupon has been validated, then $coupon->code is already a bulk code. if (isset($coupon->valid)) { $prefix = drupal_substr($coupon->code, 0, strlen($coupon->code) - $coupon->data['bulk_length']); } else { $prefix = $coupon->code; } $id = str_pad(dechex($id), strlen(dechex($coupon->data['bulk_number'])), '0', STR_PAD_LEFT); $length = strlen($prefix) + $coupon->data['bulk_length']; return strtoupper(substr($prefix . $id . md5($coupon->bulk_seed . $id), 0, $length)); } /** * Load a coupon (single or bulk) from the supplied code. * @param $code * The coupon code to search for. * @param $reset * If TRUE the cache of codes for this request will be purged. Any function which modifies * a coupon should purge the cache. */ function uc_coupon_find($code, $reset = FALSE) { // This is expensive and can be called many times during coupon processing, so we // use a simple static cache. static $cached = array(); if ($reset) { $cached = array(); } if (!$code) { return FALSE; } elseif (array_key_exists($code, $cached)) { return $cached[$code]; } // Look for matching single coupon first. $coupon = db_query("SELECT cid FROM {uc_coupons} WHERE code = :code AND status = 1 AND bulk = 0 AND valid_from < :now AND (valid_until = 0 OR valid_until > :now)", array(':code' => $code, ':now' => REQUEST_TIME)) ->fetchObject(); if ($coupon) { $cached[$code] = uc_coupon_load($coupon->cid); return $cached[$code]; } // Look through bulk coupons. $result = db_query("SELECT cid, code, data, bulk_seed FROM {uc_coupons} WHERE status = 1 AND bulk = 1 AND valid_from < :now AND (valid_until = 0 OR valid_until > :now)", array(':now' => REQUEST_TIME)); foreach ($result as $coupon) { // Check coupon prefix. $prefix_length = strlen($coupon->code); if (substr($code, 0, $prefix_length) != $coupon->code) { continue; } if ($coupon->data) { $coupon->data = unserialize($coupon->data); } // Check coupon sequence ID. $id = substr($code, $prefix_length, strlen(dechex($coupon->data['bulk_number']))); if (!preg_match("/^[0-9A-F]+$/", $id)) { continue; } $id = hexdec($id); if ($id < 0 || $id > $coupon->data['bulk_number']) { continue; } // Check complete coupon code. if ($code == uc_coupon_get_bulk_code($coupon, $id)) { $cached[$code] = uc_coupon_load($coupon->cid); return $cached[$code]; } } $cached[$code] = FALSE; return $cached[$code]; } /** * Adds or updates a coupon code for the current session. * * @param $code * The code to add or update. * @param $op * Specifies the way the code should be handled the next time session codes are validated. * - 'submit' - If the code fails validation it is removed from the session; otherwise it is retained. * A success or failure message is displayed. This is the default operation performed when * a customer enters a coupon code manually. * - 'retain' - The code remains in the session whether or not it passes validation. A success * message is displayed the first time the coupon passes. This is useful for * codes which are added automatically in response to events occurring before any products * have been added to the cart (e.g. via the querystring). * - 'auto' - The code is removed from the session whether or not it passes validation. However, if * it does validate, the corresponding coupon will be considered valid for the current request only. * This is useful for modules implementing hook_uc_coupon_revalidate(), which can decide whether or not * to add their codes each time the valid coupon cache is rebuilt (e.g. automatic discounts based on * additional conditions). */ function uc_coupon_session_add($code, $op = 'submit') { if (!variable_get('uc_coupon_allow_multiple', FALSE)) { $_SESSION['uc_coupons'] = array($code => $op); } else { $_SESSION['uc_coupons'][$code] = $op; } } /** * Removes one (or all) coupon codes from the session. * * @param $code * The code to remove, or NULL to remove all codes. * @param $is_update * TRUE (the default) if removing this code represents an update of the session; that is, if the code * was previously validated. FALSE otherwise (e.g. for removal of automatic discounts). Ignored if * a specific code is not specified. */ function uc_coupon_session_clear($code = NULL) { if (isset($code)) { unset($_SESSION['uc_coupons'][$code]); } else { unset($_SESSION['uc_coupons']); } } /** * Checks to see if a given code is present in the session, or returns an associative array * of all codes in the session. * * @param $code * (optional) The code to chec for. If not specified, will return all codes. * * @return * If a code is specified, returns TRUE if that code exists in the session, FALSE otherwise. If no * code is specified, returns an array of the form $code=>$op for all codes in the session. */ function uc_coupon_session_get($code = NULL) { if (isset($code)) { return isset($_SESSION['uc_coupons'][$code]); } elseif (isset($_SESSION['uc_coupons'])) { return $_SESSION['uc_coupons']; } else { return array(); } } /** * Validates all coupons in the current session. The validated coupons are statically * cached for each request. The cache is rebuilt the first time this function is called, * or every time the cart contents are rebuilt. * * @param $order * An order against which to validate the currently applied codes. If specified * the cached list of valid coupons is rebuilt by revalidating all the codes in * the session against that order. * * @return * An array of fully validated coupon objects, indexed by code. */ function uc_coupon_session_validate($order = NULL) { static $valids = NULL; // If a list of products is specified, then rebuild the list. if (isset($order)) { $valids = array(); $order = clone $order; // We don't want to modify the order passed in. // Allow modules an opportunity to add or remove coupons from the session. module_invoke_all('uc_coupon_revalidate', $order); // Fetch all codes in the session. $session = uc_coupon_session_get(); if (!empty($session)) { // Process all coupons in the session. global $user; $order->data['coupons'] = array(); foreach ($session as $code => $op) { $coupon = uc_coupon_validate($code, $order, $user); if ($coupon->valid) { // Process valid coupons. $valids[$code] = $coupon; $order->data['coupons'][$code] = $coupon->discounts; switch ($op) { case 'submit': case 'retain': // For coupons which were not valid (new submissions) we notify user and modules. drupal_set_message($coupon->message); module_invoke_all('uc_coupon_apply', $coupon); // And we mark them for revalidation. uc_coupon_session_add($code, 'revalidate'); break; case 'auto': // Automatic coupons are never added to the session. uc_coupon_session_clear($code); break; } } else { // Process invalid coupons. switch ($op) { case 'submit': // For new coupon submissions, just issue an error and remove from session. drupal_set_message($coupon->message, 'error'); uc_coupon_session_clear($code); break; case 'revalidate': if (!empty($products)) { // Only issue a message if the cart is not empty. drupal_set_message(t('%title is no longer applicable to your order', array('%title' => $coupon->title))); } module_invoke_all('uc_coupon_remove', $coupon); // Keep code in the session in case it becomes valid again. uc_coupon_session_add($code, 'retain'); break; case 'auto': // Automatic coupons are never added to the session. uc_coupon_session_clear($code); break; } } } } } // If no argument specified and the cache has not been built, we rebuild the cart to force validation. elseif (!isset($valids)) { uc_cart_get_contents(); } return $valids; } /** * Validates a list of coupon codes against a specified order and account. * * @param $codes * The codes to be validated. * @param $order * The order that the coupon is being applied to. * If NULL, the current cart contents will be used. * If FALSE, product and order validation will be bypassed. * @param $account * The user who is attempting to use the coupon. * If NULL, the current user will be assumed. * If FALSE, user validation will be bypassed. * * @see uc_coupon_validate() * @see uc_coupon_session_validate() */ function uc_coupon_validate_multiple($codes, $order, $account) { $order = clone $order; // We don't want to modify the order passed in. $order->data['coupons'] = array(); $valids = array(); $invalids = array(); foreach ($codes as $code) { $coupon = uc_coupon_validate($code, $order, $account); if ($coupon->valid) { // Process valid coupons. $valids[$code] = $coupon; $order->data['coupons'][$code] = $coupon->discounts; } else { $invalids[$code] = $code; } } return array('valid' => $valids, 'invalid' => $invalids); } /** * Validate a coupon, and optionally calculate the order discount. * * @param $code * The coupon code entered at the checkout screen. * @param $order * The order that the coupon is being applied to. * If NULL, the current cart contents will be used. * If FALSE, product and order validation will be bypassed. * @param $account * The user who is attempting to use the coupon. * If NULL, the current user will be assumed. * If FALSE, user validation will be bypassed. * * @return * A coupon object with extended information about the validation: * - $coupon->valid: TRUE if the code was valid, FALSE otherwise. * - $coupon->code: The specific code to be applied (even for bulk coupons). * - $coupon->title: The line item title for the discount. * - $coupon->message: A message to be displayed accepting the acceptance or rejection of this coupon. * - $coupon->amount: If $order !== FALSE, the discount that should be applied. * - $coupon->discounts: if $order !== FALSE, an array discounts on individual products indexed by nid, * containing the following fields: * -> 'discount' = The full value of the discount on that item. * -> 'pretax_discount' => The actual pre-tax discount. For fixed discounts to products with * taxes included, we apply the face value of the coupon tax-inclusively also; that is, * the actual discount is calculated so that the face value is correct after taxes. */ function uc_coupon_validate($code, $order = NULL, $account = NULL) { global $user; if (is_null($order)) { $order = new stdClass(); $order->products = uc_cart_get_contents(); } if (is_null($account)) { $account = $user; } // Look for an active coupon matching the code. $code = trim(strtoupper($code)); $coupon = uc_coupon_find($code); if (!$coupon) { $coupon = new stdClass(); $coupon->valid = FALSE; $coupon->message = t('This coupon code is invalid or has expired.'); $coupon->title = t('Unknown'); return $coupon; } // Count usage for this coupon. $uid = !empty($account) ? $account->uid : NULL; // If the order exists, don't count it towards coupon usage. $oids = !empty($order->order_id) ? array($order->order_id) : array(); $coupon->usage = uc_coupon_count_usage($coupon->cid, $uid, $oids); // Calculate the discounts (if any). uc_coupon_prepare($coupon, $code, uc_coupon_calculate_discounts($coupon, $order)); // Invoke validation hook. foreach (module_implements('uc_coupon_validate') as $module) { $callback = $module . '_uc_coupon_validate'; $result = $callback($coupon, $order, $account); if ($result === TRUE) { // This module wishes the coupon to be accepted. $coupon->valid = TRUE; } elseif (!is_null($result)) { // This module wishes the coupon to be rejected. $coupon->valid = FALSE; $coupon->message = $result; } } // Create a success message. if ($coupon->valid && !isset($coupon->message)) { if (isset($coupon->data['apply_message'])) { $coupon->message = token_replace(check_plain($coupon->data['apply_message']), array('uc_coupon' => $coupon)); } else { $amount = theme('uc_price', array('price' => $coupon->amount)); if (isset($order) || variable_get('uc_coupon_show_in_cart', TRUE)) { $coupon->message = t('A discount of !amount has been applied to your order.', array('!amount' => $amount)); } else { $coupon->message = t('A discount of !amount will be applied at checkout.', array('!amount' => $amount)); } } } return $coupon; } /** * Prepares a coupon for validation and application to an order. * * @param $coupon * A raw coupon object. * @param $discounts * An associative array of the discounts to be applied, keyed by nid or -lid. Or a string * containing a message indicating why there are no discounts available. * * @return * A fully validated coupon object with all additional properties set. This is returned * for convenience, as the $coupon provided is passed by reference and modified directly. * * @see uc_coupon_validate(). */ function uc_coupon_prepare($coupon, $code, $discounts) { $coupon->code = $code; $coupon->valid = TRUE; $coupon->amount = 0; $coupon->pretax_amount = 0; if (!is_array($discounts)) { $coupon->discounts = array(); $coupon->message = $discounts; } else { $coupon->discounts = $discounts; foreach ($coupon->discounts as $item) { $coupon->amount += $item->discount; $coupon->pretax_amount += isset($item->pretax_discount) ? $item->pretax_discount : $item->discount; } $coupon->amount = round($coupon->amount, variable_get('uc_currency_prec', 2)); unset($coupon->message); } // Create the line item title for this coupon. $format = !empty($coupon->data['line_item_format']) ? $coupon->data['line_item_format'] : variable_get('uc_coupon_line_item_format', t('Coupon !code', array('!code' => '[uc_coupon:code]'))); $coupon->title = token_replace(check_plain($format), array('uc_coupon' => $coupon)); return $coupon; } /** * Implements hook_uc_coupon_validate(). * * We implement our own hook to allow other modules a chance to run before us. * * @param $coupon * The coupon object to validate, with special fields set as follows: * - $coupon->code: The specific code to be applied (even for bulk coupons). * - $coupon->amount: If $order !== FALSE, the discount that should be applied. * - $coupon->usage: Coupon usage data from uc_coupon_count_usage(). * @param $order * The order against which this coupon is to be applied, or FALSE to bypass * order validation. * @param $account * The account of the user trying to use the coupon, or FALSE to bypass user * validation. * * @return * TRUE if the coupon should be accepted. * NULL to allow other modules to determine validation. * Otherwise, a string describing the reason for failure. */ function uc_coupon_uc_coupon_validate(&$coupon, $order, $account) { // Coupons which produce no discount are not valid unless they are store credit // type, or have no face value (e.g. free shipping). if ($coupon->type !== 'credit' && $coupon->value != 0 && $coupon->amount == 0) { $coupon->valid = FALSE; return !empty($coupon->message) ? $coupon->message : t('This coupon is not applicable to your order.'); } // Check for allowed combinations. if (!empty($order->data['coupons'])) { foreach (array_keys($order->data['coupons']) as $code) { $other = uc_coupon_find($code); $other_listed = !empty($coupon->data['combinations']) && in_array($other->cid, $coupon->data['combinations']); $this_ok = (isset($coupon->data['negate_combinations']) xor $other_listed); $this_listed = !empty($other->data['combinations']) && in_array($coupon->cid, $other->data['combinations']); $other_ok = (isset($other->data['negate_combinations']) xor $this_listed); if (!$this_ok || !$other_ok) { return t('This coupon combination is not allowed.'); } } } if ($coupon->type !== 'credit') { // Check maximum usage per code. if ($coupon->max_uses > 0 && !empty($coupon->usage['codes'][$coupon->code]) && $coupon->usage['codes'][$coupon->code] >= $coupon->max_uses) { return t('This coupon has reached the maximum redemption limit.'); } // Check maximum usage per user. if ($account && isset($coupon->data['max_uses_per_user']) && $coupon->usage['user'] >= $coupon->data['max_uses_per_user']) { return t('This coupon has reached the maximum redemption limit.'); } } else { if (!empty($coupon->usage['value']['codes'][$coupon->code]) && $coupon->usage['value']['codes'][$coupon->code] >= $coupon->value) { return t('This coupon has reached the maximum redemption limit.'); } } // Check user ID. if ($account && isset($coupon->data['users'])) { if (in_array("$account->uid", $coupon->data['users'], TRUE) xor !isset($coupon->data['negate_users'])) { return t('Your user ID is not allowed to use this coupon.'); } } // Check roles. if ($account && isset($coupon->data['roles'])) { $role_found = FALSE; foreach ($coupon->data['roles'] as $role) { if (in_array($role, $account->roles)) { $role_found = TRUE; break; } } if ($role_found xor !isset($coupon->data['negate_roles'])) { return t('You do not have the correct permission to use this coupon.'); } } } /** * Find items that a coupon will apply to and calculate the discounts. * * @param $coupon * A coupon object to apply, or a coupon code as a string. * @param $order * The order object to which the coupon should be applied. * * @return * An array of discounts. */ function uc_coupon_calculate_discounts($coupon, $order) { // Can only calculate discounts if an order is provided. if (empty($order)) { return array(); } if (!is_object($coupon)) { // If argument is a code, load the corresponding coupon. $coupon = uc_coupon_find($coupon); } // Discover if any items match the restrictions, and which items the discount should be calculated against. $restricted = isset($coupon->data['products']) || isset($coupon->data['skus']) || isset($coupon->data['terms']) || isset($coupon->data['product_types']); $matched = 0; $matched_price = 0; $total_qty = 0; $total_price = 0; $items = array(); foreach ($order->products as $item) { if (isset($item->module) && $item->module == 'uc_coupon') { continue; } $node = node_load($item->nid); $qty = $item->qty; if (!$restricted) { // Coupons with no restrictions apply to all products. $include = TRUE; } else { // Other coupons only apply to matching products. $include = FALSE; $terms = _uc_coupon_list_terms($node); if (isset($coupon->data['products']) && isset($item->data['kit_id'])) { // Items that are part of product kits must be included or excluded all together, so we pre-empt other restrictions. $include = (isset($coupon->data['negate_products']) xor in_array($item->data['kit_id'], $coupon->data['products'])); } else if (isset($coupon->data['products']) && (isset($coupon->data['negate_products']) xor in_array($item->nid, $coupon->data['products']))) { $include = TRUE; } elseif (isset($coupon->data['products']) && isset($coupon->data['negate_products']) && in_array($item->nid, $coupon->data['products'])) { // always exclude if in list of negated products } elseif (isset($coupon->data['terms']) && (isset($coupon->data['negate_terms']) xor count(array_intersect($terms, $coupon->data['terms'])))) { $include = TRUE; } elseif (isset($coupon->data['terms']) && isset($coupon->data['negate_terms']) && count(array_intersect($terms, $coupon->data['terms']))) { // always exclude if one of the terms is in the list of negated terms } elseif (isset($coupon->data['skus']) && _uc_coupon_match_sku($item->model, $coupon->data['skus'])) { $include = TRUE; } elseif (isset($coupon->data['product_types']) && in_array($node->type, $coupon->data['product_types'])) { $include = TRUE; } } // A matching product was found. if ($include) { $matched += $qty; $matched_price += $item->price * $qty; } $total_qty += $qty; $total_price += $item->price * $qty; // Include this item. Coupons that apply to the order subtotal affect all products. if ($include || $coupon->data['apply_to'] == 'subtotal') { $clone = clone $item; $clone->type = $node->type; $items = array_pad($items, count($items) + $qty, $clone); } } // If no matches were found, there are no discounts to calculate. if ($matched == 0) { return t('You do not have any applicable products in your cart.'); } $use_matched = (isset($coupon->data['minimum_qty_restrict']) && $coupon->data['minimum_qty_restrict'] != FALSE); // Make sure the minimum quantity restriction (if any) is met. if (isset($coupon->data['minimum_qty'])) { if (($use_matched ? $matched : $total_qty) < (int)$coupon->data['minimum_qty']) { return t('You do not have enough applicable products in your cart.'); } } // Make sure the minimum order total restriction (if any) is met. if ($coupon->minimum_order > 0) { if (($use_matched ? $matched_price : $total_price) < $coupon->minimum_order) { return $use_matched ? t('You have not reached the minimum total of applicable products for this coupon.') : t('You have not reached the minimum order total for this coupon.'); } } // Ensure that all products match, if specified. if (isset($coupon->data['require_match_all']) && $matched < $total_qty) { return t('You have non-applicable products in your cart'); } // Slice off applicable products if a limit was set. switch ($coupon->data['apply_to']) { case 'cheapest': usort($items, '_uc_coupon_sort_products'); $items = array_slice($items, 0, $coupon->data['apply_count']); break; case 'expensive': usort($items, '_uc_coupon_sort_products'); $items = array_slice($items, -$coupon->data['apply_count']); break; } // Build the discounts array and get the order total. $total = 0; $discounts = array(); $included_rates = array(); foreach ($items as $item) { if (!isset($discounts[$item->nid])) { // First entry for this product. // Calculate the pre-tax discount proportion for this item. // For fixed discounts to products with taxes included, we apply the face value of the coupon // tax-inclusively also; that is, the actual discount is reduced so that the face value is // realized after taxes. (This already happens automatically for percentage based coupons). $included_rate = 1; if (module_exists('uc_taxes')) { foreach (uc_taxes_rate_load() as $tax) { if ($tax->display_include && is_array($tax->taxed_line_items) && in_array('coupon', $tax->taxed_line_items) && in_array($item->type, $tax->taxed_product_types) && ($tax->shippable == 0 || $item->data['shippable'] == 1)) { $included_rate += $tax->rate; } } } // Adjust the price for any stacked coupons. $prior_discount = 0; if (!empty($order->data['coupons'])) { foreach ($order->data['coupons'] as $stacked) { if (isset($stacked[$item->nid])) { $prior_discount += $stacked[$item->nid]->pretax_discount; } } } $total -= $prior_discount * $included_rate; $discounts[$item->nid] = (object) array( 'qty' => 1, 'price' => $item->price - $prior_discount, ); $included_rates[$item->nid] = $included_rate; unset($item->type); } else { // An entry for this product already exists. // Add this item to the total for the product. $discounts[$item->nid]->price += $item->price; $discounts[$item->nid]->qty++; } $total += $item->price * $included_rate; } // Add in discounts for any included line items. $items = uc_order_load_line_items($order); if (!empty($order->line_items) && !empty($coupon->data['line_items'])) { foreach ($order->line_items as $line_item) { if (in_array($line_item['type'], $coupon->data['line_items'])) { // Use a negative id to distinguish this from a product discount. $lid = $line_item['line_item_id']; $lid = is_numeric($lid) ? -$lid : $lid; // No tax-inclusive line items in ubercart (yet). $included_rate = 1; // Adjust the price for any stacked coupons. $prior_discount = 0; if (!empty($order->data['coupons'])) { foreach ($order->data['coupons'] as $stacked) { if (isset($stacked[$lid])) { $prior_discount += $stacked[$lid]->pretax_discount; } } } $discounts[$lid] = (object) array( 'qty' => 1, 'price' => $line_item['amount'] - $prior_discount, ); $included_rates[$lid] = $included_rate; $total += $discounts[$lid]->price * $included_rate; } } } // Calculate the discounts per item. $value = $coupon->value; if ($coupon->type === 'credit' && !empty($coupon->usage['value']['codes'][$coupon->code])) { $value -= $coupon->usage['value']['codes'][$coupon->code]; } foreach ($discounts as $id => $discount) { $inclusive_price = $discount->price * $included_rates[$id]; switch ($coupon->type) { case 'percentage': $discount->discount = $inclusive_price * $coupon->value / 100; break; case 'set_price': $discount->discount = max($inclusive_price - ($coupon->value * $discount->qty), 0); break; default: if ($coupon->type === 'credit' || $coupon->data['apply_to'] == 'subtotal' || $coupon->data['apply_to'] == 'products_total') { // Apply single discount proportionally across all matching items. $discount->discount = $total == 0 ? 0 : min($value * ($inclusive_price / $total), $inclusive_price); } else { // Apply full discount value to each matching item. $discount->discount = min($value * $discount->qty, $inclusive_price); } } $discount->pretax_discount = $discount->discount / $included_rates[$id]; unset($discount->price); unset($discount->qty); } return $discounts; } function _uc_coupon_match_sku($model, $skus) { foreach ($skus as $match) { if (preg_match('/^' . str_replace('\*', '.*?', preg_quote($match, '/')) . '$/', $model)) { return TRUE; } } return FALSE; } function _uc_coupon_sort_products($a, $b) { if ($a->price == $b->price) { return 0; } return $a->price > $b->price ? 1 : -1; } /** * Lists all taxonomy terms contained in 'taxonomy_term_reference' fields for a given node. * @param $node * The node whose terms should be listed; * @return * An array of any taxonomy term id's. */ function _uc_coupon_list_terms($node) { $terms = array(); foreach(array_keys(field_info_instances('node', $node->type)) as $field_name) { $field_info = field_info_field($field_name); if ($field_info['type'] == 'taxonomy_term_reference') { if ($field_values = field_get_items('node', $node, $field_name)) { foreach ($field_values as $field_value) { $terms[] = $field_value['tid']; } } } } return $terms; } /** * Implements hook_block_info(). */ function uc_coupon_block_info() { $blocks = array(); $blocks['coupon-discount'] = array( 'info' => t('Coupon discount form'), ); return $blocks; } /** * Implements hook_block_view(). */ function uc_coupon_block_view($delta) { if ($delta == 'coupon-discount') { $block = array( 'subject' => t('Coupon discount'), 'content' => drupal_get_form('uc_coupon_form', 'block'), ); return $block; } } /** * Default theme implementation for the coupon submit form. */ function theme_uc_coupon_form($variables) { $form = $variables['form']; $output = ''; if ($form['#uc_coupon_form_context'] == 'cart') { $output .= '