'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 .= '

' . t('Coupon discounts') . '

'; } elseif ($form['#uc_coupon_form_context'] == 'block') { if (isset($form['code'])) { $form['code']['#size'] = 15; } } $output .= drupal_render_children($form); return $output; } /** * Implements hook_uc_cart_pane(). */ function uc_coupon_uc_cart_pane($items) { drupal_add_css(drupal_get_path('module', 'uc_coupon') . '/uc_coupon.css'); // The coupon entry cart pane. $body = drupal_get_form('uc_coupon_form', 'cart') + array( '#prefix' => '
', '#suffix' => '
' ); $panes[] = array( 'id' => 'coupon', 'body' => $body, 'title' => t('Coupon discount'), 'desc' => t('Allows shoppers to use a coupon during checkout for order discounts.'), 'weight' => 1, 'enabled' => TRUE, ); // The "Special Discounts" cart pane. $body = array(); $discounts = _uc_coupon_options_list(uc_coupon_session_validate(), FALSE); if (!empty($discounts)) { $body = array( '#theme' => 'uc_coupon_automatic_discounts', '#prefix' => '
', '#title' => t('Special discounts'), '#suffix' => '
', 'discounts' => array( '#theme' => 'item_list', '#items' => _uc_coupon_options_list(uc_coupon_session_validate(), FALSE), '#title' => t('Special discounts'), ) ); } $panes[] = array( 'id' => 'coupon_auto', 'body' => $body, 'title' => t('Special Discounts'), 'desc' => t('Displays a list of automatic discounts.'), 'weight' => 1, 'enabled' => TRUE, ); return $panes; } function theme_uc_coupon_automatic_discounts($variables) { $form = $variables['form']; /* $items = $form['discounts']['#items']; $title = isset($form['discounts']['#title']) ? $form['discounts']['#title'] : NULL; $rows = array(); foreach($items as $item) { $rows[] = array($item); } $form['discounts'] = array( '#theme' => 'table', '#rows' => $rows, ); if (isset($title)) { $form['discounts']['#header'] = array($title); } */ return drupal_render_children($form); } /** * Create a tapir table of validated coupons with a "Remove" button for each. * * @param $coupons * An array of coupon options of the form code => title * @param $submit * An array of additional options to attach to the form's submit elements (e.g. #ajax, #submit) */ function uc_coupon_table($coupons, $submit = FALSE) { $table = array( '#type' => 'tapir_table', ); $table['#columns'] = array( 'title' => array( 'cell' => t('Active coupons'), 'weight' => 0, ), 'remove' => array( 'cell' => t('Remove'), 'weight' => 1, ), ); $i = 0; foreach ($coupons as $code => $title) { $table[$i] = array( 'title' => array('#markup' => $title), 'remove' => array( '#type' => 'submit', '#value' => t('Remove'), '#name' => 'uc-coupon-remove-' . $code, ), ); if ($submit) { // Add ajax functionality to this table. $table[$i]['remove'] += $submit; } $i++; } return $table; } /** * Helper function to create a list of coupons for form elements. * @param $coupons * An array of validated coupon objects to include in the list. * @param $in_session * TRUE to include only coupons currently added to the session. * FALSE to include those not in the session (e.g. automatic coupons). * @return * An associative array mapping coupon code to coupon title. */ function _uc_coupon_options_list($coupons, $in_session = TRUE) { $options = array(); if (!empty($coupons)) { foreach ($coupons as $coupon) { if (!$in_session xor uc_coupon_session_get($coupon->code)) { $options[$coupon->code] = $coupon->title; if ($coupon->type === 'credit') { $credit = empty($coupon->usage['value']['codes'][$coupon->code]) ? 0 : $coupon->usage['value']['codes'][$coupon->code]; $credit += empty($coupon->amount) ? 0 : $coupon->amount; $credit = $credit > $coupon->value ? 0 : $coupon->value - $credit; $options[$coupon->code] .= ' (' . t('@credit credit remaining', array('@credit' => uc_currency_format($credit))) . ')'; } } } } return $options; } function uc_coupon_checkout_submit($form, &$form_state) { $form_state['rebuild'] = TRUE; unset($form_state['checkout_valid']); $form_state['redirect'] = 'cart/checkout'; } function uc_coupon_order_submit($form, &$form_state) { $form_state['rebuild'] = TRUE; uc_coupon_form_submit($form, $form_state); $coupons = uc_coupon_get_order_coupons($form_state['order']); uc_coupon_apply_to_order($form_state['order'], uc_coupon_get_order_coupons($form_state['order'])); // !TODO: Shouldn't save the order here because we prevent reverting changes // made by other panes? uc_order_save($form_state['order']); } /** * Form builder for the uc_coupon form. * * @param $context * Where the form is to appear: 'cart', 'block' or 'checkout' * @param $submit * An array of additional options to attach to the form's submit elements (e.g. #ajax, #submit) */ function uc_coupon_form($form, $form_state, $context = 'block', $submit = FALSE) { //dpm($form_state['order'], 'order build'); $coupons = ($context == 'order') ? uc_coupon_get_order_coupons($form_state['order']) : uc_coupon_session_validate(); //dpm($coupons, 'coupons build'); $components = variable_get('uc_coupon_form_components', drupal_map_assoc(variable_get('uc_coupon_allow_multiple', FALSE) ? array('entry') : array('entry', 'list'))); // Show the coupon code entry component if (!empty($components['entry'])) { $form['code'] = array( '#type' => 'textfield', '#size' => 25, '#title' => t('Coupon Code'), '#description' => t('Enter a coupon code and click "Apply to order" below.'), ); $form['apply'] = array( '#type' => 'submit', '#value' => t('Apply to order'), '#name' => 'uc-coupon-apply', ); if (!variable_get('uc_coupon_allow_multiple', FALSE) && count(uc_coupon_session_get()) > 0) { $form['code']['#description'] .= ' ' . t('Apply a blank code to remove the currently applied coupon.'); } drupal_add_css('#uc-coupon-active-coupons, #uc-coupon-other-discounts { clear: left; }', array('type' => 'inline', 'group' => CSS_DEFAULT)); if ($submit) { $form['apply'] += $submit; } } // Add active coupons components (table and/or list). $options = _uc_coupon_options_list($coupons, $context != 'order'); if (!empty($options)) { if (!empty($components['table'])) { $form['coupons_table'] = tapir_get_table('uc_coupon_table', $options, $submit); } if (!empty($components['list'])) { $form['coupons'] = array( '#prefix' => '
', '#suffix' => '
', '#type' => 'checkboxes', '#title' => t('Active Coupons'), '#options' => $options, '#default_value' => array_keys($options), '#description' => t('These coupons have been applied to your order. To remove one, uncheck the box and click "Remove coupons" below.'), ); $form['coupons']['#description'] = t('These coupons have been applied to your order. To remove one, uncheck the box next to the coupon name and click "Update order" below.'); $form['remove'] = array( '#type' => 'submit', '#value' => t('Update order'), '#name' => 'uc-coupon-remove', ); if ($submit) { $form['remove'] += $submit; } } } // Add context to help out themers. $form['#uc_coupon_form_context'] = $context; return $form; } /** * Implements hook_uc_order(). */ function uc_coupon_uc_order($op, &$order) { if ($op == 'presave') { // Apply any session coupons to the current cart order. if (_uc_coupon_is_checkout_order($order)) { $coupons = uc_coupon_session_validate($order); uc_coupon_apply_to_order($order, $coupons); } // Make sure any fake cart items don't get saved with the order if the checkout page is skipped // (e.g. Paypal Express Checkout, Google Checkout) foreach ($order->products as $key => $product) { if (isset($product->module) && $product->module == 'uc_coupon') { unset($order->products[$key]); } } } } /** * Apply a set of coupons to an order. * * Line items and entries in the uc_coupons_orders table will be added for each coupon. Any coupon line items * or entries which are not in the list of coupons will be removed. Additionally, the coupons' discount * arrays will be added to the order object's data array. * * @param $order * The order to which the coupons should be applied. * @param $coupons * An associative array of fully validated coupon objects, keyed by the coupon code. */ function uc_coupon_apply_to_order($order, $coupons) { // Index existing line items by coupon code. $items = array(); foreach ($order->line_items as $index => $line) { if ($line['type'] == 'coupon') { // For orders created before multi-coupons were enabled, the code was not saved with the line item. // In this case, we retreive it from the uc_coupons_orders table. $code = isset($line['data']['code']) ? $line['data']['code'] : db_query('SELECT code FROM {uc_coupons_orders} WHERE oid = :oid', array(':oid' => $order_id))->fetchField(); $items[$code] = $index; } } // Index existing entries in {uc_coupons_orders} by coupon code. $entries = db_query('SELECT code, cuid FROM {uc_coupons_orders} WHERE oid = :oid', array(':oid' => $order->order_id))->fetchAllKeyed(0,1); // Update, insert or delete line items and entries in uc_coupons_orders. $insert = array(); $order->data['coupons'] = array(); foreach($coupons as $coupon) { $order->data['coupons'][$coupon->code] = $coupon->discounts; // Handle entries in {uc_coupons_orders}. if (isset($entries[$coupon->code])) { db_update('uc_coupons_orders')->condition('cuid', $entries[$coupon->code]) ->fields(array('cid' => $coupon->cid, 'value' => $coupon->amount)) ->execute(); unset($entries[$coupon->code]); } else { $insert[] = array($coupon->cid, $order->order_id, $coupon->code, $coupon->value); } // Handle line items. if (isset($items[$coupon->code])) { $line =& $order->line_items[$items[$coupon->code]]; $line['title'] = $coupon->title; $line['amount'] = -$coupon->pretax_amount; $line['data']['code'] = $coupon->code; uc_order_update_line_item($line['line_item_id'], $line['title'], $line['amount'], $line['data']); unset($items[$coupon->code]); } else { // Create a new line item. $order->line_items[] = uc_order_line_item_add($order->order_id, 'coupon', $coupon->title, -$coupon->pretax_amount, _uc_line_item_data('coupon', 'weight'), array('code' => $coupon->code) ); } } // Insert new entries in {uc_coupons_orders} if (!empty($insert)) { $query = db_insert('uc_coupons_orders'); $query->fields(array('cid', 'oid', 'code', 'value')); foreach ($insert as $fields) { $query->values($fields); } $query->execute(); } // Delete orphaned entries in {uc_coupons_orders} if (!empty($entries)) { db_delete('uc_coupons_orders')->condition('cuid', $entries)->execute(); } // Remove orphaned line-items. foreach ($items as $index) { uc_order_delete_line_item($order->line_items[$index]['line_item_id']); unset($order->line_items[$index]); } usort($order->line_items, 'uc_weight_sort'); } /** * Implements hook_uc_checkout_pane(). * * Show a pane just above the order total that allows shoppers to enter a coupon * for a discount. */ function uc_coupon_uc_checkout_pane() { $panes[] = array( 'id' => 'coupon', 'callback' => 'uc_checkout_pane_coupon', 'title' => t('Coupon discount'), 'desc' => t('Allows shoppers to use a coupon during checkout for order discounts.'), 'weight' => 5, 'process' => TRUE, ); $panes[] = array( 'id' => 'coupon_automatic', 'callback' => 'uc_checkout_pane_coupon_automatic', 'title' => t('Special Discounts'), 'desc' => t('Displays a list of all automatic coupon discounts.'), 'weight' => 5, 'process' => FALSE, ); return $panes; } /** * Ajax callback for checkout form. */ function uc_coupon_checkout_update($form, $form_state) { $commands[] = ajax_command_replace('#coupon-pane', trim(drupal_render($form['panes']['coupon']))); if (isset($form['panes']['coupon_automatic'])) { $commands[] = ajax_command_replace('#coupon_automatic-pane', trim(drupal_render($form['panes']['coupon_automatic']))); } if (isset($form['panes']['quotes'])) { $commands[] = ajax_command_replace('#quotes-pane', drupal_render($form['panes']['quotes'])); } if (isset($form['panes']['payment'])) { $commands[] = ajax_command_replace('#payment-pane', trim(drupal_render($form['panes']['payment']))); } if (variable_get('uc_coupon_show_in_cart', TRUE) && isset($form['panes']['cart']['cart_review_table'])) { $commands[] = ajax_command_html('#cart-pane>div', drupal_render($form['panes']['cart']['cart_review_table'])); } // Clear the coupon code, but only if the submission was successful. if (count(drupal_get_messages('error', FALSE)) == 0) { $commands[] = ajax_command_invoke('#coupon-pane input[type=text]', 'val', array('')); } // Make sure all checkboxes are checked. $commands[] = ajax_command_invoke('#coupon-pane input[type=checkbox]', 'attr', array('checked', 'true')); // Show any messages. $commands[] = ajax_command_html('#coupon-messages', theme('status_messages')); return array('#type' => 'ajax', '#commands' => $commands); } /** * A checkout pane listing any automatic discounts. */ function uc_checkout_pane_coupon_automatic($op, &$order, $form = NULL, &$form_state = NULL) { if ($op == 'view') { $discounts = _uc_coupon_options_list(uc_coupon_session_validate(), FALSE); if (empty($discounts)) { $inner_contents = array( '#markup' => t('None.'), ); } else { $inner_contents = array( '#theme' => 'item_list', '#items' => _uc_coupon_options_list(uc_coupon_session_validate(), FALSE) ); } return array( 'theme' => 'uc_coupon_automatic_discounts', 'contents' => array( 'discounts' => $inner_contents, ), ); } } /** * Checkout Pane callback function. * * Used to display a form in the checkout process so that customers * can enter discount coupons. */ function uc_checkout_pane_coupon($op, &$order, $form = NULL, &$form_state = NULL) { switch ($op) { case 'prepare': // Remove fake cart items from the order. foreach ($order->products as $key => $product) { if (isset($product->module) && $product->module == 'uc_coupon') { unset($order->products[$key]); } } break; case 'view': // Revalidate the session coupons against the actual order. drupal_add_css('#coupon-messages { clear: both; }', array('type' => 'inline', 'group' => CSS_DEFAULT)); $description = variable_get('uc_coupon_pane_description', t('Enter a coupon code for this order.')); $submit = array( '#limit_validation_errors' => array(), '#ajax' => array( 'callback' => 'uc_coupon_checkout_update', ), '#submit' => array('uc_coupon_checkout_submit'), ); $contents = uc_coupon_form(array(), $form_state, 'checkout', $submit); $contents['message'] = array( '#markup' => '
', '#weight' => 2, ); return array( 'description' => $description, 'contents' => $contents, 'theme' => 'uc_coupon_form', ); case 'process': $trigger = $form_state['triggering_element']['#name']; if (substr($trigger, 0, 9) == 'uc-coupon') { $form_state['rebuild'] = TRUE; uc_coupon_form_submit($form['panes']['coupon'], $form_state); return FALSE; // Prevent redirection. } else { // !TODO Coupon will not be submitted if "Apply to order" is not clicked. Is this what we want? return TRUE; } case 'settings': $form['uc_coupon_collapse_pane'] = array( '#type' => 'checkbox', '#title' => t('Collapse checkout pane by default.'), '#default_value' => variable_get('uc_coupon_collapse_pane', FALSE), ); $form['uc_coupon_pane_description'] = array( '#type' => 'textarea', '#title' => t('Checkout pane message'), '#default_value' => variable_get('uc_coupon_pane_description', t('Enter a coupon code for this order.')), ); return $form; } } /** * Submit handler for the uc_coupon form. */ function uc_coupon_form_submit($form, &$form_state) { $trigger = $form_state['triggering_element']['#name']; // Determine where the values are (they will be in a subarray if called from checkout or order page). switch ($context = $form['#uc_coupon_form_context']) { case 'checkout': $values = $form_state['values']['panes']['coupon']; break; case 'order': $values = $form_state['values']['coupon']; break; default: $values = $form_state['values']; } // If this was the result of a 'remove' submission. if (substr($trigger, 0, 16) == 'uc-coupon-remove') { // See if there was an individual remove button clicked. $code = substr($trigger, 17); if (!empty($code)) { if ($context == 'order') { unset($form_state['order']->data['coupons']['code']); } else { uc_coupon_session_clear($code); drupal_set_message(t('Coupon "%code" has been removed from your order', array('%code' => $code))); module_invoke_all('uc_coupon_remove', uc_coupon_find($code)); } } // Otherwise see if it's a checkbox submission. elseif (isset($values['coupons'])) { $removed = array(); foreach ($values['coupons'] as $code => $selected) { if (!$selected) { $removed[] = $code; if ($context == 'order') { unset($form_state['order']->data['coupons'][$code]); } else { uc_coupon_session_clear($code); module_invoke_all('uc_coupon_remove', uc_coupon_find($code)); } } } $n = count($removed); if ($n > 1) { $last = $removed[$n - 1]; $rest = implode(', ', array_slice($removed, 0, $n - 1)); drupal_set_message(t('Coupons %rest and %last have been removed from your order.', array('%rest' => $rest, '%last' => $last))); } elseif (!empty($removed)) { drupal_set_message(t('Coupon %code has been removed from your order', array('%code' => $removed[0]))); } } } // Otherwise try to apply. else { $code = empty($values['code']) ? '' : strtoupper(trim($values['code'])); $removed = FALSE; // If multiple codes are not enabled, then remove any codes currently applied. if (!variable_get('uc_coupon_allow_multiple', FALSE) && count($session = uc_coupon_session_get()) > 0) { if ($context == 'order') { unset($form_state['order']->data['coupons']); } else { foreach (array_keys($session) as $remove_code) { uc_coupon_session_clear($remove_code); drupal_set_message(t('Coupon "%code" has been removed from your order', array('%code' => $remove_code))); module_invoke_all('uc_coupon_remove', uc_coupon_find($remove_code)); } $removed = TRUE; } } if (!empty($code)) { if ($context == 'order') { $coupon = uc_coupon_validate($code, $form_state['order'], user_load($form_state['order']->uid)); if ($coupon->valid) { $form_state['order']->data['coupons'][$code] = $coupon->discounts; } else { drupal_set_message($coupon->message, 'error'); } } else { uc_coupon_session_add($code, 'submit'); } } elseif (!$removed) { drupal_set_message(t("You must enter a valid coupon code."), 'error'); } } } /** * Implements hook_uc_line_item(). */ function uc_coupon_uc_line_item() { $items[] = array( 'id' => 'coupon', 'title' => t('Coupon discount'), 'tax_adjustment' => 'uc_coupon_tax_adjustment', 'weight' => 0, 'default' => FALSE, 'stored' => TRUE, 'add_list' => FALSE, 'calculated' => TRUE, ); return $items; } /** * Handle tax on coupons by calculating tax for individual discounted prices. */ function uc_coupon_tax_adjustment($price, $order, $tax) { $amount = 0; if (isset($order->data['coupons'])) { foreach ($order->data['coupons'] as $discounts) { foreach ($discounts as $id => $item) { if (is_numeric($id) && $id > 0) { // This is a product discount, so see if the product is taxable. $node = node_load($id); $adjust = in_array($node->type, $tax->taxed_product_types) && ($tax->shippable == 0 || $node->shippable == 1); } else { // This is a line-item discount, so find the corresponding line item. $lid = is_numeric($id) ? -$id : $id; // Convert id to a line item id. foreach ($order->line_items as $line_item) { if ($line_item['line_item_id'] == $lid) { $adjust = in_array($line_item['type'], $tax->taxed_line_items); break; } } } if ($adjust) { $amount += (isset($item->pretax_discount) ? $item->pretax_discount : $item->discount) * ($price > 0 ? 1 : -1); } } } } return $amount; } /** * Show a message if PayPal is enabled and "itemized order" is selected. */ function _uc_coupon_paypal_check() { if (variable_get('uc_payment_method_paypal_wps_checkout', 0) && variable_get('uc_paypal_wps_submit_method', 'single') == 'itemized') { drupal_set_message(t('To use coupons with PayPal you must select "Submit the whole order as a single line item". Click here to change this setting.', array('!url' => url('admin/store/settings/payment/edit/methods')))); } } /** * Implements hook_uc_store_status(). */ function uc_coupon_uc_store_status() { $statuses = array(); if (variable_get('uc_payment_method_paypal_wps_checkout', 0) && variable_get('uc_paypal_wps_submit_method', 'single') == 'itemized') { $statuses[] = array( 'status' => 'warning', 'title' => t('Coupons'), 'desc' => t('To use coupons with PayPal you must select "Submit the whole order as a single line item". Click here to change this setting.', array('!url' => url('admin/store/settings/payment/edit/methods'))), ); } return $statuses; } /** * Implements hook_uc_cart_alter(). * * This is called every time the cart is rebuild (e.g. when products are added), so it's a good place * to revalidate our session coupons. We also add a fake cart item (if configured to show in cart) * for each coupon. These will be removed at checkout. */ function uc_coupon_uc_cart_alter(&$items) { // Validate all codes in the session against the cart contents. $order = new UcOrder(); $order->products = $items; $order->data = array(); $coupons = uc_coupon_session_validate($order); if (variable_get('uc_coupon_show_in_cart', TRUE) && !empty($coupons)) { // If there are some valid coupons, then add them to the cart (but only if // they have a non-zero value. foreach ($coupons as $code => $coupon) { if ($coupon->amount != 0) { $items[] = _uc_coupon_cart_item($coupon); } } } } /** * Creates a fake cart-item corrresponding to this coupon, allowing this coupon to be displayed in the cart. * * @param $coupon * The coupon to be displayed in the cart. */ function _uc_coupon_cart_item($coupon) { // Exclude any line-item discounts from the amount shown in the cart. $amount = 0; foreach ($coupon->discounts as $id => $discount) { if (is_numeric($id) && $id > 0) { $amount += $discount->discount; } } // Assign this a unique cart_item_id so it will be keyed properly by entity_view(). $id = -hexdec(substr(sha1($coupon->code), -8)); return (object) array( 'cart_item_id' => $id, 'module' => 'uc_coupon', 'title' => $coupon->title, 'nid' => 0, 'qty' => 1, 'price' => -$amount, 'data' => array('module' => 'uc_coupon', 'shippable' => FALSE, 'code' => $coupon->code, 'remove' => uc_coupon_session_get($coupon->code)), 'model' => 0, 'weight' => 0 ); } /** * Implements hook_uc_cart_display(). */ function uc_coupon_uc_cart_display($item) { $display_item = array( 'module' => array('#type' => 'value', '#value' => 'uc_coupon'), 'nid' => array('#type' => 'value', '#value' => 0), 'title' => array('#markup' => $item->title), 'description' => array('#markup' => ''), 'qty' => array('#type' => 'hidden', '#value' => 1, '#default_value' => 1), '#total' => $item->price, 'data' => array('#type' => 'hidden', '#value' => serialize($item->data)), '#suffixes' => array(), ); if ($item->data['remove']) { $display_item['remove'] = array('#type' => 'submit', '#value' => t('Remove')); } return $display_item; } /** * Implements hook_uc_update_cart_item(). * Remove a coupon from the order when the "Remove" button is clicked. */ function uc_coupon_uc_update_cart_item($nid, $data, $qty) { if (isset($data['code']) && $qty == 0) { uc_coupon_session_clear($data['code']); module_invoke_all('uc_coupon_remove', uc_coupon_find($data['code'])); } } /** * Theme override for the default cart block content. * Removes coupons from the total number of items. */ function uc_coupon_theme_uc_cart_block_content($variables) { if (variable_get('uc_coupon_show_in_cart', TRUE) && !empty($variables['items'])) { foreach ($variables['items'] as &$item) { if ($item['nid'] == 0 && $item['price'] <= 0) { $item['qty'] = ''; $variables['item_count']--; } } $variables['item_text'] = format_plural($variables['item_count'], '1 Item', '@count Items'); } return theme_uc_cart_block_content($variables); } /** * Implements hook_form_FORM_ID_alter() for uc_cart_checkout_form(). * * Remove any coupon cart items from the serialized cart contents and payment-pane * order, as coupons will be handled as line items during checkout. * * Collapse coupon checkout pane, if configured to do so. */ function uc_coupon_form_uc_cart_checkout_form_alter(&$form, $form_state) { if (variable_get('uc_coupon_collapse_pane', FALSE) && isset($form['panes']['coupon'])) { $form['panes']['coupon']['#collapsed'] = TRUE; } // Show current session coupons in the cart pane (since now they will have been removed from the order). if (variable_get('uc_coupon_show_in_cart', TRUE) && isset($form['panes']['cart'])) { $coupons = uc_coupon_session_validate(); // If there are some valid coupons, then add them to the cart. foreach ($coupons as $code => $coupon) { if ($coupon->amount != 0) { $item = _uc_coupon_cart_item($coupon); $item->order_product_id = $item->cart_item_id; $form['panes']['cart']['cart_review_table']['#items'][] = $item; } } } } /** * Implements hook_uc_checkout_complete(). * * Ensure the stored coupon code is reset after checkout. */ function uc_coupon_uc_checkout_complete($order, $account) { uc_coupon_session_clear(); } /** * Preprocess template for a printed coupon certificate. * @see uc_coupon-certificate.tpl.php */ function template_preprocess_uc_coupon_certificate(&$variables) { $coupon = $variables['coupon']; // Create variables for each user-added field. $fields = field_info_fields(); foreach ($fields as $name => $field) { if (in_array('uc_coupon', array_keys($field['bundles']))) { $items = field_get_items('uc_coupon', $coupon, $name); $variables[$name] = $items; } } $variables['value'] = theme('uc_coupon_discount', array('coupon' => $coupon)); $variables['display_name'] = check_plain($coupon->name); $n = stripos($variables['display_name'], 'purchased by'); if ($n) { $variables['display_name'] = substr($variables['display_name'], 0, $n -1); } if ($coupon->valid_until) { $variables['not_yet_valid'] = $coupon->valid_from > REQUEST_TIME; $variables['valid_from'] = format_date($coupon->valid_from, 'custom', variable_get('date_format_uc_store', 'm/d/Y')); $variables['valid_until'] = format_date($coupon->valid_until, 'custom', variable_get('date_format_uc_store', 'm/d/Y')); } else { $variables['not_yet_valid'] = FALSE; $variables['valid_from'] = FALSE; $variables['valid_until'] = FALSE; } $variables['max_uses_per_user'] = isset($coupon->data['max_uses_per_user']) ? $coupon->data['max_uses_per_user'] : NULL; $variables['include'] = array(); $variables['exclude'] = array(); if (isset($coupon->data['product_types'])) { foreach ($coupon->data['product_types'] as $type) { $variables['include'][] = node_type_get_name($type); } } if (isset($coupon->data['products'])) { $key = isset($coupon->data['negate_products']) ? 'exclude' : 'include'; foreach ($coupon->data['products'] as $nid) { $node = node_load($nid); $variables[$key][] = $node->title; } } if (isset($coupon->data['skus'])) { foreach ($coupon->data['skus'] as $sku) { $variables['include'][] = t('SKU') . ' ' . $sku; } } if (isset($coupon->data['terms'])) { $key = isset($coupon->data['negate_terms']) ? 'exclude' : 'include'; foreach ($coupon->data['terms'] as $tid) { $term = taxonomy_term_load($tid); $variables[$key][] = $term->name; } } // Merge in global tokens. $info = token_info(); foreach ($info['types'] as $type => $type_info) { if (empty($type_info['needs-data']) && $type != 'current-user' && $type != 'current-date') { $type_key = !empty($type_info['type']) ? $type_info['type'] : $type; if (!empty($info['tokens'][$type_key])) { foreach (array_keys($info['tokens'][$type_key]) as $token) { $variables[str_replace('-', '_', $type_key) . '_' . str_replace('-', '_', $token)] = token_replace("[$type_key:$token]"); } } } } if (isset($variables['coupon']->data['base_cid'])) { $variables['theme_hook_suggestions'][] = 'uc_coupon_certificate__base_' . $variables['coupon']->data['base_cid']; } $variables['theme_hook_suggestions'][] = 'uc_coupon_certificate__' . $variables['coupon']->cid; } /** * Page template for printed coupons. * @see uc_coupon-page.tpl.php */ function template_preprocess_uc_coupon_page(&$variables) { $variables['styles'] = drupal_get_css(); } /** * Implements hook_uc_coupon_actions(). */ function uc_coupon_uc_coupon_actions($coupon) { $actions = array(); if (user_access('view store coupons')) { $actions[] = array( 'url' => 'admin/store/coupons/' . $coupon->cid, 'icon' => drupal_get_path('module', 'uc_store') . '/images/order_view.gif', 'title' => t('View coupon: @name', array('@name' => $coupon->name)), ); $actions[] = array( 'url' => 'admin/store/coupons/' . $coupon->cid . '/print', 'icon' => drupal_get_path('module', 'uc_store') . '/images/print.gif', 'title' => t('Print coupon: @name', array('@name' => $coupon->name)), ); if ($coupon->bulk) { $actions[] = array( 'url' => 'admin/store/coupons/' . $coupon->cid . '/codes', 'icon' => drupal_get_path('module', 'uc_store') . '/images/menu_reports_small.gif', 'title' => t('Download codes as CSV: @name', array('@name' => $coupon->name)), ); } } if (user_access('manage store coupons')) { $actions[] = array( 'url' => 'admin/store/coupons/' . $coupon->cid . '/edit', 'icon' => drupal_get_path('module', 'uc_store') . '/images/order_edit.gif', 'title' => t('Edit coupon: @name', array('@name' => $coupon->name)), ); $actions[] = array( 'url' => 'admin/store/coupons/' . $coupon->cid . '/delete', 'icon' => drupal_get_path('module', 'uc_store') . '/images/order_delete.gif', 'title' => t('Delete coupon: @name', array('@name' => $coupon->name)), ); } return $actions; } /** * Implements hook_views_api(). */ function uc_coupon_views_api() { return array( 'api' => '2.0', 'path' => drupal_get_path('module', 'uc_coupon') . '/views', ); } /** * Check whether an order is the order being checked out by the current user. * @param $order */ function _uc_coupon_is_checkout_order($order) { global $user; return isset($_SESSION['cart_order']) && isset($order->order_id) && $order->order_id == $_SESSION['cart_order'] && uc_order_status_data($order->order_status, 'state') == 'in_checkout' && $user->uid == $order->uid; } /** * Implements hook_entity_info(); */ function uc_coupon_entity_info() { return array( 'uc_coupon' => array( 'label' => t('Coupon'), 'controller class' => 'UcCouponController', 'metadata controller class' => 'UcCouponMetadataController', 'base table' => 'uc_coupons', 'fieldable' => TRUE, 'entity keys' => array( 'id' => 'cid', ), 'bundles' => array( 'uc_coupon' => array( 'label' => t('Coupon'), 'admin' => array( 'path' => 'admin/store/settings/coupon', 'access arguments' => array('manage store coupons'), ), ), ), 'view modes' => array( 'full' => array( 'label' => t('Administrative view'), ), ), ), ); } /** * Loads one coupon entity from the database. */ function uc_coupon_load($cid, $reset = FALSE) { if (is_null($cid) || $cid < 1) { return FALSE; } $coupons = uc_coupon_load_multiple(array($cid), array(), $reset); return $coupons ? reset($coupons) : FALSE; } /** * Loads one or more coupon entities from the database. * * @param $ids * An array of coupon IDs. * @param $conditions * An array of conditions on the {uc_coupons} table in the form * 'field' => $value. * * @return * An array of order objects indexed by order_id. */ function uc_coupon_load_multiple($ids, $conditions = array(), $reset = FALSE) { return entity_load('uc_coupon', $ids, $conditions, $reset); } /** * Save a coupon object. * * If the 'cid' field is set, then this will update an existing coupon. * Otherwise, a new bulk seed will be generated, the coupon will be * inserted into the database, and $coupon->cid will be set. * * @param $coupon * The coupon to save. * * @param $edit * An optional array of extra data that other modules may need to save. */ function uc_coupon_save(&$coupon, $edit = array()) { entity_save('uc_coupon', $coupon); } /** * Delete a coupon object. * * @param $cid * The id of the coupon to delete. */ function uc_coupon_delete($cid) { entity_delete('uc_coupon', $cid); } /** * Implements hook_field_extra_fields(). */ function uc_coupon_field_extra_fields() { $extra = array(); $extra['uc_coupon']['uc_coupon']['display']['admin_summary'] = array( 'label' => t('Administrative Summary'), 'description' => t('A summary of all coupon details.'), 'weight' => 0, ); return $extra; } /** * Implements hook_uc_order_pane(). * * Defines the shipping quote order pane. */ function uc_coupon_uc_order_pane() { $panes['coupon'] = array( 'callback' => 'uc_order_pane_coupon', 'title' => t('Coupon, Credit or Discount Codes'), 'desc' => t('Apply a coupon or discount code to the current order.'), 'class' => 'pos-left', 'weight' => 7, 'show' => array('edit'), ); return $panes; } /** * Coupon order pane callback. * * @see uc_quote_order_pane_quotes_submit() * @see uc_quote_apply_quote_to_order() */ function uc_order_pane_coupon($op, $order, &$form = NULL, &$form_state = NULL) { switch ($op) { case 'edit-form': $submit = array( '#limit_validation_errors' => array(array('coupon')), '#submit' => array('uc_coupon_order_submit'), ); $form['coupon'] = uc_coupon_form(array(), $form_state, 'order', $submit); $form['#uc_coupon_form_context'] = 'order'; $form['coupon']['#theme'] = 'uc_coupon_form'; $form['coupon']['#tree'] = TRUE; break; case 'edit-theme': return drupal_render($form['coupon']); } } /** * Implements hook_form_uc_order_edit_form_alter(). */ function uc_coupon_form_uc_order_edit_form_alter(&$form, &$form_state) { $order = $form_state['order']; $line_items = $order->line_items; foreach ($line_items as $item) { // Coupon line items should be changed using the coupon order-edit pane. if ($item['type'] == 'coupon') { $form['line_items'][$item['line_item_id']]['title'] = array( '#markup' => check_plain($item['title']), ); $form['line_items'][$item['line_item_id']]['remove']['#access'] = FALSE; $form['line_items'][$item['line_item_id']]['amount'] = array( '#theme' => 'uc_price', '#price' => $item['amount'], ); } } } /** * Gets the fully validated coupon objects that have been applied to this order. * * @param $order * The order in question. * @param $recalculate * If TRUE, the value of each coupon will be recalculated using the current state * of the order and current coupon settings. * If FALSE (default), the original coupon values will be preserved. */ function uc_coupon_get_order_coupons($order, $recalculate = FALSE) { $coupons = array(); if (!empty($order->data['coupons'])) { if ($recalculate) { $dummy_order = clone $order; $dummy_order->data['coupons'] = array(); } foreach ($order->data['coupons'] as $code => $discounts) { $coupon = uc_coupon_find($code); if (!empty($coupon->cid)) { if ($recalculate) { $discounts = uc_coupon_calculate_discounts($coupon, $dummy_order); $dummy_order->data['coupons'][$code] = $coupon->discounts; } $coupons[] = uc_coupon_prepare($coupon, $code, $discounts); } } } return $coupons; }