%d) AND d.is_active = 1 AND d.discount_type = %d ORDER BY weight", time(), time(), DISCOUNT_TYPE_FREE_ITEMS); while ($discount = db_fetch_object($result)) { if (in_array($product->nid, get_product_ids_for_discount_object($discount, DISCOUNT_FILTER_GROUPING_QUALIFICATION, TRUE))) { $new_product_ids = get_product_ids_for_discount_object($discount, DISCOUNT_FILTER_GROUPING_APPLICATION, TRUE); break; } } if (!empty($new_product_ids)) { foreach ($new_product_ids as $id) { $potential_product = node_load($id); // How many of the item are in the cart? We're only interested in node IDs right now. $items = uc_cart_get_contents(); $qty = 0; $source_qty = 0; foreach ($items as $item) { if ($item->nid == $potential_product->nid) { $qty += $item->qty; } if ($item->nid == $product->nid) { $source_qty += $item->qty; } } $target_qty = $discount->discount_amount; $qualifying_amount = $discount->qualifying_amount; $times_applied = floor($qty / $target_qty); $times_to_apply = ((($target_qty / $qualifying_amount) * $added_qty) / $target_qty); // Make sure max_times_applied is respected if ($times_applied < $discount->max_times_applied || $discount->max_times_applied == 0) { // Calculate how many there should be and subtract what we already have $to_add = ($target_qty * ($discount->max_times_applied ? min($discount->max_times_applied - $times_applied, $times_to_apply) : $times_to_apply)); } // Don't add items immediately so that uc_cart_get_contents() will behave predictably. if ($to_add > 0) { $items_to_add[] = array('nid' => $potential_product->nid, 'qty' => $to_add, 'data' => array()); } } } foreach ($items_to_add as $p) { uc_cart_add_item($p['nid'], $p['qty'], $p['data'] + module_invoke_all('add_to_cart_data', $p), NULL, FALSE, FALSE, FALSE); } } } /** * Implementation of hook_perm(). */ function uc_discounts_perm() { return array('configure discounts'); } /** * Implementation of hook_menu(). */ function uc_discounts_menu() { $items = array(); $items['admin/store/uc_discounts'] = array( 'title' => 'Discounts', 'description' => 'Add and review discounts.', 'page callback' => 'uc_discounts_admin_list', 'access arguments' => array('configure discounts'), 'type' => MENU_NORMAL_ITEM, 'file' => 'uc_discounts.admin.inc', ); $items['admin/store/uc_discounts/list'] = array( 'title' => 'List', 'description' => 'View list of discounts.', 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => 0, ); $items['admin/store/uc_discounts/add'] = array( 'title' => 'Add discount', 'page callback' => 'drupal_get_form', 'page arguments' => array('uc_discounts_form'), 'access arguments' => array('configure discounts'), 'type' => MENU_LOCAL_TASK, 'weight' => 1, 'file' => 'uc_discounts.admin.inc', ); $items['admin/store/uc_discounts/edit/%'] = array( 'title' => 'Edit discount rule', 'page callback' => 'drupal_get_form', 'page arguments' => array('uc_discounts_form', 4), 'access arguments' => array('configure discounts'), 'type' => MENU_CALLBACK, 'file' => 'uc_discounts.admin.inc', ); $items['admin/store/uc_discounts/copy/%'] = array( 'page callback' => 'uc_discounts_copy', 'page arguments' => array(4), 'access arguments' => array('configure discounts'), 'type' => MENU_CALLBACK, 'file' => 'uc_discounts.admin.inc', ); $items['admin/store/uc_discounts/delete/%'] = array( 'title' => 'Delete discount rule', 'page callback' => 'drupal_get_form', 'page arguments' => array('uc_discounts_delete', 4), 'access arguments' => array('configure discounts'), 'type' => MENU_CALLBACK, 'file' => 'uc_discounts.admin.inc', ); $items['admin/store/uc_discounts/generate_codes/%'] = array( 'title' => 'Generate codes', 'page callback' => 'drupal_get_form', 'page arguments' => array('uc_discounts_generate_codes_form', 4), 'access arguments' => array('configure discounts'), 'type' => MENU_CALLBACK, 'file' => 'uc_discounts.admin.inc', 'weight' => 2, ); $items['cart/checkout/uc_discounts/calculate'] = array( 'page callback' => 'uc_discounts_js_calculate', 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); $items['admin/reports/uc_discounts/all'] = array( 'title' => 'Ubercart discounts usage data', 'description' => 'View usage data for each Ubercart discount.', 'page callback' => 'uc_discounts_report', 'access arguments' => array('access site reports'), 'file' => 'uc_discounts.admin.inc', ); $items['admin/reports/uc_discounts/all/download'] = array( 'title' => 'Download Ubercart discounts usage data', 'description' => 'Download usage data for each Ubercart discount.', 'page callback' => 'uc_discounts_report', 'access arguments' => array('access site reports'), 'file' => 'uc_discounts.admin.inc', 'type' => MENU_CALLBACK, ); $items['admin/reports/uc_discounts/discount'] = array( 'title' => 'Ubercart discount usage data', 'page callback' => 'uc_discounts_report_for_discount', 'access arguments' => array('access site reports'), 'type' => MENU_CALLBACK, 'file' => 'uc_discounts.admin.inc', ); return $items; } /** * Implementation of hook_order(). * * Manages order->uc_discounts_codes (array of code) and order->discounts (array of uses) */ function uc_discounts_order($op, &$arg1, $arg2) { switch ($op) { case 'load': //Get order's codes from database $arg1->uc_discounts_codes = uc_discounts_get_codes_for_order($arg1->order_id); break; case 'save': //If discount line items need updating if ($arg1->uc_discounts_line_items_need_updating) { //Delete existing order codes uc_discounts_order_codes_delete($arg1->order_id); //Save order's codes as string $codes_string = uc_discounts_codes_to_str($arg1->uc_discounts_codes); $new_discount_order_code = array('order_id' => $arg1->order_id, 'codes' => $codes_string); drupal_write_record('uc_discounts_order_codes', $new_discount_order_code); //Get order line items if (is_array($arg1->line_items)) { $existing_line_items = $arg1->line_items; } else { $existing_line_items = uc_order_load_line_items($arg1, TRUE); } // Use new_order_line_items to populate $arg1->line_items by: // Storing all non-discounts line items // Storing new discount line items $new_order_line_items = array(); //Delete existing discount line items foreach ($existing_line_items as $line_item) { // Allow LINE_ITEM_KEY_NAME suffixes SCUK if (strpos($line_item["type"], LINE_ITEM_KEY_NAME) !== false) { uc_order_delete_line_item($line_item["line_item_id"]); } //Otherwise store non-discount line item else { $new_order_line_items[] = $line_item; } } //Add discount line items foreach ($arg1->uc_discounts_line_items as $line_item) { if ($line_item['amount'] != 0) { uc_order_line_item_add($arg1->order_id, $line_item['type'], $line_item['title'], $line_item['amount'], $line_item['weight'], $line_item['data']); $new_order_line_items[] = $line_item; } } //Update order line items (see new_order_line_items notes above) $arg1->line_items = $new_order_line_items; //Force tax recalculation (currently unused but may be required if line item weights change) //module_invoke("uc_taxes", "order", "save", $arg1, ""); $arg1->uc_discounts_line_items_need_updating = FALSE; } break; case 'update': //If status changes to "cancelled", delete order uses if ($arg2 == "cancelled") { uc_discounts_uses_delete_for_order($arg1->order_id); } break; // Ensure stored discounts are accurate (recalculate and match new amounts against stored amounts) case 'submit': $result = uc_discounts_apply($arg1, FALSE); if (!$result['success']) { return array(array('pass' => FALSE, 'message' => $result['message'])); } break; case 'delete': // Delete existing order codes uc_discounts_order_codes_delete($arg1->order_id); //TO DO: determine if uses should be deleted or put global setting in for user to decide break; } } /** * Implementation of hook_uc_checkout_complete(). * Upon successful completion of checkout, record usage * Note: $order->uc_discounts_codes is set by the checkout pane */ function uc_discounts_uc_checkout_complete($order, $account) { uc_discounts_uses_save_for_order($order); // Don't allow user to re-use codes on another order without re-entering them unset($_SESSION['uc_discounts_codes']); } /** * Implementation of hook_add_to_cart() */ function uc_discounts_add_to_cart($nid, $qty, $data) { // @see hook_init() $_SESSION['uc_discounts_after_add_to_cart'] = array('nid' => $nid, 'qty' => $qty); } /** * Implementation of Ubercart's hook_line_item(). * * Displays all discounts as a single line item * If uc_vat and uc_taxes modules exist and enabled: display a discount line item per VAT type SCUK * * @see hook_line_item() */ function uc_discounts_line_item() { // If uc_vat module exists and enabled, add a line item per VAT type if (module_exists('uc_vat') && module_exists('uc_taxes')) { $taxes = uc_taxes_rate_load(); foreach ($taxes as $tax) { $line_items[] = array( "id" => LINE_ITEM_KEY_NAME . $tax->id, "title" => t("Discount") . ' ' . check_plain($tax->name), "weight" => LINE_ITEM_WEIGHT, "stored" => TRUE, // Added to total "calculated" => TRUE, "display_only" => FALSE, ); } } else { // If not, single line item $line_items[] = array( "id" => LINE_ITEM_KEY_NAME, "title" => t("Discount"), "weight" => LINE_ITEM_WEIGHT, "stored" => TRUE, // Added to total "calculated" => TRUE, "display_only" => FALSE, ); } return $line_items; } /** * Implementation of hook_order_pane */ function uc_discounts_order_pane() { $panes[] = array( 'id' => 'uc_discounts', 'callback' => 'uc_discounts_order_pane_callback', 'title' => t('Discount codes'), 'weight' => 8, 'show' => array('edit'), ); return $panes; } /** * Callback from hook_order_pane */ function uc_discounts_order_pane_callback($op, $arg1) { switch ($op) { case 'edit-form': $form['uc-discounts']['uc-discounts-codes'] = array( '#type' => 'textarea', '#rows' => 3, ); $form['uc-discounts']['uc-discounts-button'] = array( '#type' => 'submit', '#value' => t('Apply discounts'), ); return $form; case 'edit-theme': $form_rendered = drupal_render($arg1['uc-discounts']); return $form_rendered; case 'edit-ops': return array(t('Apply discounts')); case t('Apply discounts'): if ($order = uc_order_load($arg1['order_id'])) { $order->uc_discounts_codes = array_merge($order->uc_discounts_codes, uc_discounts_codes_to_array($arg1['uc-discounts-codes'])); $result = uc_discounts_apply($order, TRUE, FALSE); drupal_set_message($result['message'], ($result['success'] ? 'notice' : 'error')); /* $has_code_errors = FALSE; $errors = array(); $warnings = array(); $discounts = get_discounts_for_order($order, $errors, $warnings); foreach ($errors as $error) { drupal_set_message($error, 'error'); } foreach ($warnings as $warning) { drupal_set_message($warning, 'warning'); } if (empty($errors)) { if (empty($discounts)) { drupal_set_message(t('The code(s) did not yield a discount for this order.'), 'warning'); } else { add_discount_line_items_to_order($order, $discounts); $order->uc_discounts_line_items_need_updating = TRUE; uc_discounts_order('save', $order, NULL); uc_discounts_uses_save_for_order($order); drupal_set_message(t('Discount code(s) applied to order.')); } } */ } break; } } /** * Returns the price for a product including Tax, if using uc_vat module * * @param object $item */ function get_uc_price($item, $revision = 'altered') { $price_info = array( 'price' => $item->price, 'qty' => $item->qty, ); $context = array( 'type' => 'cart_item', 'revision' => $revision, 'subject' => array( 'cart_item' => $item, 'node' => node_load($item->nid), ), ); return uc_price($price_info, $context); } /** * Implementation of hook_cart_pane(). * * @see hook_cart_pane() */ function uc_discounts_cart_pane($items) { $panes[] = array( 'id' => 'uc_discounts_pane', 'title' => t('Discounts'), 'enabled' => TRUE, 'weight' => 1, 'body' => !is_null($items) ? uc_discounts_cart_pane_output($items) : '', ); return $panes; } /** * Adds the discounts to the cart page as line items via javascript */ function uc_discounts_cart_pane_output($items) { global $user; //Create phony order object to call to get_discounts_for_order $order = new stdClass(); $order->uid = $user->uid; $order->products = $items; $errors = array(); $warnings = array(); $messages = array(); $discounts = get_discounts_for_order($order, $errors, $warnings, $messages); //If there are no discounts, do not alter cart if (count($discounts) == 0) { return ''; } //Calculate subtotal with discounts $subtotal = 0; if (is_array($items)) { // Used in /cart foreach ($items as $item) $subtotal += get_uc_price($item); } $total_discount_amount = 0; if (is_array($discounts)) { foreach ($discounts as $discount) { $total_discount_amount += $discount->total; } } $subtotal_including_discounts = $subtotal - $total_discount_amount; //Add total discount message $messages[] = "". t("Total discount") .": ". uc_currency_format($total_discount_amount); //Add new subtotal message $messages[] = "". t("Subtotal including discounts") .": ". uc_currency_format($subtotal_including_discounts); //Start row index at item count + 2 (1 for subtotal row in cart form, 1 more for our first row) $i = count($items) + 2; //Create table to hold discount messages $body = "
"; foreach ($messages as $message) { $evenOddClass = (($i % 2) == 0) ? "even" : "odd"; $body .= sprintf(""; $i += 1; } //Close table $body .= "
", $evenOddClass, "uc-discounts-cart-pane-table-cell") . $message ."
"; //Write table using javascript between items and cart form buttons drupal_add_js(sprintf('$(document).ready(function() { $("#cart-form-buttons").before("%s"); });', $body), 'inline'); } /** * Implementation of hook_checkout_pane(). * * @see hook_checkout_pane() */ function uc_discounts_checkout_pane() { $panes[] = array( "id" => "uc_discounts", "callback" => "uc_checkout_pane_discounts", 'process' => TRUE, "title" => t("Enter discount codes"), "weight" => 5, ); return $panes; } /** * Implementation of hook_form_FORM_ID_alter() * Ensures that javascript for adding discounts is added to checkout, * regardless of whether discounts pane is used. */ function uc_discounts_form_uc_cart_checkout_form_alter(&$form, $form_state) { drupal_add_js(array( 'uc_discounts' => array( 'url' => url('cart/checkout/uc_discounts/calculate'), 'line_item_key_name' => LINE_ITEM_KEY_NAME, 'line_item_weight' => LINE_ITEM_WEIGHT, 'total_discount_text' => t('Total discount'), 'calculate_discount_response_line_items_key' => CALCULATE_DISCOUNT_RESPONSE_LINE_ITEMS_KEY, 'calculate_discount_response_errors_key' => CALCULATE_DISCOUNT_RESPONSE_ERRORS_KEY, 'calculate_discount_response_messages_key' => CALCULATE_DISCOUNT_RESPONSE_MESSAGES_KEY, // discount_rates_ids are used to distinguish the different discount line items SCUK 'discount_rates_ids' => DISCOUNT_RATES_IDS_KEY, 'progress_msg' => t('Calculating discounts...'), 'no_codes_entered' => t('Please enter at least one code'), 'no_applicable_discounts' => t('No applicable discounts'), 'err_msg' => t('There were problems determining if any discounts apply. Please try again shortly. If this does not resolve the issue, please call @phone to complete your order.', array('@phone' => variable_get('uc_store_phone', NULL)) ), 'response_parse_err_msg' => t('Unable to parse response text: '), ), ), 'setting'); drupal_add_js('$(document).ready(function(e) { uc_discountsOnLoad(e); });', 'inline'); drupal_add_js('misc/progress.js'); drupal_add_js(drupal_get_path('module', 'uc_discounts') .'/uc_discounts.js'); } /** * Discounts checkout pane callback * * More information at http://www.ubercart.org/docs/developer/245/checkout */ function uc_checkout_pane_discounts($op, &$arg1, $arg2) { global $user; switch ($op) { case "view": $description = t("Enter discount codes in the box below (one per line)."); //If viewing an existing order, load order's codes if (!empty($arg1->order_id)) { $codes_string = uc_discounts_codes_to_str(uc_discounts_get_codes_for_order($arg1->order_id)); } $contents["uc-discounts-codes"] = array( "#type" => "textarea", "#default_value" => $codes_string, "#rows" => 5, "#prefix" => "
", "#suffix" => "
", ); $contents["uc-discounts-placeholder"] = array( "#type" => "hidden", "#prefix" => "
", "#suffix" => "
", ); $contents["uc-discounts-button"] = array( "#type" => "button", "#value" => t("Click to calculate discounts"), ); return array("description" => $description, "contents" => $contents); case "process": // Save form values from checkout pane in order ($arg1). $arg1->uc_discounts_codes = uc_discounts_codes_to_array($arg2['uc-discounts-codes']); drupal_alter('uc_discounts_codes', $arg1, 'pane_submit'); $has_code_errors = FALSE; $errors = array(); $warnings = array(); $discounts = get_discounts_for_order($arg1, $errors, $warnings); // Make sure the recorded discount codes for the order were only the ones that were actually used // for a discount. $applied_codes = array(); foreach ($discounts as $discount) { $applied_codes[] = $discount->code; } $arg1->uc_discounts_codes = $applied_codes; foreach ($errors as $error) { drupal_set_message($error, "error"); } foreach ($warnings as $warning) { drupal_set_message(t('Warning: @warning', array('@warning' => $warning)), 'error'); } //If there were errors, return FALSE if (!empty($errors)) { return FALSE; } //Add discount line items to order add_discount_line_items_to_order($arg1, $discounts); //Mark order as needing discount line items updated $arg1->uc_discounts_line_items_need_updating = TRUE; break; } } /** * Add discount line items to order * * Note: assumes discount objects are the result of a call to get_discounts_for_order() */ function add_discount_line_items_to_order(&$order, $discounts) { //Create line items for discounts and store in order's uc_discounts_line_items field $line_items = array(); foreach ($discounts as $discount) { $line_item = array( // If exists the rate_type, use it SCUK 'type' => empty($discount->rate_type) ? LINE_ITEM_KEY_NAME : LINE_ITEM_KEY_NAME . $discount->rate_type, 'title' => $discount->short_description, 'amount' => -$discount->amount, 'weight' => LINE_ITEM_WEIGHT, 'data' => array('discount_id' => $discount->discount_id), ); $line_items[] = $line_item; } $order->uc_discounts_line_items = $line_items; } /** * AJAX callback for discounts calculation. * * Calculate discount for an order in the checkout page. */ function uc_discounts_js_calculate() { global $user; if (!empty($_SESSION["cart_order"])) { $order_id = $_SESSION['cart_order']; $order = uc_order_load($order_id); //If session order exists, use it if (is_null($order)) { print '{}'; exit; } } //Otherwise create phony order else { $order = new stdClass(); $order->uid = $user->uid; $order->products = uc_cart_get_contents(); } $order->uc_discounts_codes = uc_discounts_codes_to_array($_POST['uc-discounts-codes']); drupal_alter('uc_discounts_codes', $order, 'js_calculate'); $line_items = array(); $errors = array(); $warnings = array(); $messages = array(); $discounts = get_discounts_for_order($order, $errors, $warnings, $messages); $i = 0; // Session vars get used by conditional action $_SESSION['uc_discounts_codes'] = $order->uc_discounts_codes; // Hold the rates ids involved in discounts SCUK $discount_rates_ids = array(); foreach ($discounts as $discount) { if ($discount->amount != 0) { $line_item = array(); // Add rate type suffix if exist $line_item["id"] = LINE_ITEM_KEY_NAME . (empty($discount->rate_type) ? '' : $discount->rate_type); // If line item id doesn't exists in array, add it' if (!in_array($line_item["id"],$discount_rates_ids)) { $discount_rates_ids[] = $line_item["id"]; } $line_item["type"] = $discount->type; $line_item["title"] = $discount->title; $line_item["amount"] = -$discount->amount; $line_item["weight"] = $discount->weight; // Add the rate type and rate name if not empty $line_item["rate_type"] = empty($discount->rate_type) ? 0 : $discount->rate_type; $line_item["rate_name"] = empty($discount->rate_name) ? "" : $discount->rate_name; $line_items[] = $line_item; } } if (!empty($warnings)) { $warnings2 = array(); foreach ($warnings as $warning) { $warnings2[] = t('Warning: @warning', array('@warning' => $warning)); } $errors = array_merge($errors, $warnings2); } $calculate_discount_response = array( CALCULATE_DISCOUNT_RESPONSE_LINE_ITEMS_KEY => $line_items, CALCULATE_DISCOUNT_RESPONSE_ERRORS_KEY => $errors, CALCULATE_DISCOUNT_RESPONSE_MESSAGES_KEY => $messages, // Pass the discount rates ids SCUK DISCOUNT_RATES_IDS_KEY => $discount_rates_ids, ); drupal_json($calculate_discount_response); exit; } /** * Implements hook_theme(). */ function uc_discounts_theme() { return array( 'uc_discounts_cart_checkout_table' => array( 'arguments' => array('form' => NULL), ), ); } /** * Implementation of hook_checkout_pane_alter() * Get discounted price to show up on checkout, & review */ function uc_discounts_checkout_pane_alter(&$panes) { foreach ($panes as &$pane) { if ($pane['id'] == 'cart') { $pane['callback'] = 'uc_discounts_checkout_pane_content'; } } } function uc_discounts_checkout_pane_content($op) { switch ($op) { case 'view': $contents['cart_review_table'] = array( '#value' => theme('uc_discounts_cart_checkout_table'), '#weight' => variable_get('uc_pane_cart_field_cart_weight', 2), ); return array('contents' => $contents, 'next-button' => FALSE); case 'review': $discount_amount = uc_discounts_get_discount_amount_for_order($op); $items = uc_cart_get_contents(); $output = ''; $context = array( 'revision' => 'themed', 'type' => 'cart_item', 'subject' => array(), ); $total = 0; foreach ($items as $item) { $total += ($item->price * $item->qty); $desc = check_plain($item->title) . uc_product_get_description($item); $price_info = array( 'price' => $item->price, 'qty' => $item->qty, ); $context['subject'] = array( 'cart' => $items, 'cart_item' => $item, 'node' => node_load($item->nid), ); $output .= ''; } if ($discount_amount > 0) { $final_price = $total - $discount_amount; $output .= ''; $output .= ''; } $output .= '
' . $item->qty . '×' . $desc . '' . uc_price($price_info, $context) . '
' . t('Discount') . ': ' . uc_price($discount_amount, $context) . '
' . t('Total') . ': ' . uc_price($final_price, $context) . '
'; $review[] = $output; return $review; } } function theme_uc_discounts_cart_checkout_table($show_subtotal = TRUE) { $subtotal = 0; $discount_amount = uc_discounts_get_discount_amount_for_order('checkout'); // Set up table header. $header = array( array('data' => t('Qty'), 'class' => 'qty'), array('data' => t('Products'), 'class' => 'products'), array('data' => t('Price'), 'class' => 'price'), ); $context = array(); // Set up table rows. $contents = uc_cart_get_contents(); foreach ($contents as $item) { $price_info = array( 'price' => $item->price, 'qty' => $item->qty, ); $context['revision'] = 'altered'; $context['type'] = 'cart_item'; $context['subject'] = array( 'cart' => $contents, 'cart_item' => $item * $discount_amount, 'node' => node_load($item->nid), ); $total = uc_price($price_info, $context); $subtotal += $total; $description = check_plain($item->title) . uc_product_get_description($item); // Remove node from context to prevent the price from being altered. $context['revision'] = 'themed-original'; $context['type'] = 'amount'; unset($context['subject']); $rows[] = array( array('data' => t('@qty×', array('@qty' => $item->qty)), 'class' => 'qty'), array('data' => $description, 'class' => 'products'), array('data' => uc_price($total, $context), 'class' => 'price'), ); } if ($discount_amount > 0) { $rows[] = array( array('data' => ''), array('data' => t('Discount:'), 'align' => 'right'), array('data' => uc_price($discount_amount, $context), 'class' => 'price'), ); $subtotal = $subtotal - $discount_amount; } // Add the subtotal as the final row. if ($show_subtotal) { $context = array( 'revision' => 'themed-original', 'type' => 'amount', ); $rows[] = array( 'data' => array(array('data' => '' . t('Subtotal:') . ' ' . uc_price($subtotal, $context), 'colspan' => 3, 'class' => 'subtotal')), 'class' => 'subtotal', ); } return theme('table', $header, $rows, array('class' => 'cart-review')); } /////////////////////////////////////////////////////////////////// //Database operations /////////////////////////////////////////////////////////////////// /** * Deletes a uc_discounts row and all dependencies. */ function uc_discounts_delete_all($discount) { foreach (module_implements('uc_discount') as $module) { $function = $module . '_uc_discount'; $function('delete', $discount); } db_query("DELETE FROM {uc_discounts_uses} WHERE discount_id=%d", $discount->discount_id); db_query("DELETE FROM {uc_discounts_products} WHERE discount_id=%d", $discount->discount_id); db_query("DELETE FROM {uc_discounts_terms} WHERE discount_id=%d", $discount->discount_id); db_query("DELETE FROM {uc_discounts_skus} WHERE discount_id=%d", $discount->discount_id); db_query("DELETE FROM {uc_discounts_roles} WHERE discount_id=%d", $discount->discount_id); db_query("DELETE FROM {uc_discounts_codes} WHERE discount_id=%d", $discount->discount_id); db_query("DELETE FROM {uc_discounts_authors} WHERE discount_id=%d", $discount->discount_id); db_query("DELETE FROM {uc_discounts} WHERE discount_id=%d", $discount->discount_id); } /** * Loads a discount */ function uc_discounts_load($discount_id) { $discount = db_fetch_object(db_query("SELECT * FROM {uc_discounts} WHERE discount_id=%d", $discount_id)); foreach (module_implements('uc_discount') as $module) { $function = $module . '_uc_discount'; $function('load', $discount); } return $discount; } /** * Returns codes for discount. */ function get_codes_for_discount($discount_id) { $codes = array(); $result = db_query('SELECT code FROM {uc_discounts_codes} WHERE discount_id = %d', $discount_id); while ($row = db_fetch_array($result)) { $codes[] = $row['code']; } return $codes; } /** * Deletes all uc_discounts_codes rows for a discount. */ function uc_discounts_codes_delete($discount_id) { $query = "DELETE FROM {uc_discounts_codes} WHERE discount_id=%d"; db_query($query, $discount_id); } /** * Returns product_ids for discount object. * Note: this function correctly returns all products for term-based discounts. * * @param $discount object * @param $exclude_all_products bool * @param $grouping const * * @return array of product ids (nids) */ function get_product_ids_for_discount_object($discount, $grouping = DISCOUNT_FILTER_GROUPING_APPLICATION, $exclude_all_products = FALSE) { $filter = $grouping == DISCOUNT_FILTER_GROUPING_APPLICATION ? $discount->filter_type : $discount->required_product_type; switch ($filter) { case FILTER_TYPE_PRODUCTS: return get_product_ids_for_discount($discount->discount_id, $grouping, $exclude_all_products); case FILTER_TYPE_TERMS: $product_ids = array(); //Get products for terms $terms = get_term_ids_for_discount($discount->discount_id, $grouping, $exclude_all_products); $query = 'SELECT DISTINCT p.nid FROM {uc_products} p'; if (!empty($terms)) { $query .= ' INNER JOIN {term_node} tn ON p.nid=tn.nid INNER JOIN {uc_discounts_terms} dt ON tn.tid=dt.term_id WHERE dt.discount_id=%d'; } $result = db_query($query, $discount->discount_id); while ($row = db_fetch_object($result)) { $product_ids[] = $row->nid; } return $product_ids; case FILTER_TYPE_SKUS: $skus = get_skus_for_discount($discount->discount_id, $grouping, $exclude_all_products); $query = 'SELECT DISTINCT p.nid FROM {uc_products} p'; if (!empty($skus)) { $query .= ' INNER JOIN {uc_discounts_skus} ds ON p.model=ds.sku WHERE ds.discount_id=%d'; } $result = db_query($query, $discount->discount_id); while ($row = db_fetch_object($result)) { $product_ids[] = $row->nid; } return $product_ids; case FILTER_TYPE_CLASS: $classes = get_classes_for_discount($discount->discount_id, $grouping, $exclude_all_products); $query = 'SELECT DISTINCT n.nid FROM {node} n'; if (!empty($classes)) { $query .= ' INNER JOIN {uc_discounts_classes} dcl ON n.type=dcl.class WHERE dcl.discount_id=%d'; } $result = db_query($query, $discount->discount_id); while ($row = db_fetch_object($result)) { $product_ids[] = $row->nid; } return $product_ids; case FILTER_TYPE_AUTHORS: $authors = get_author_ids_for_discount($discount->discount_id, $grouping, $exclude_all_products); $query = 'SELECT DISTINCT n.nid FROM {node} n'; if (!empty($authors)) { $query .= ' INNER JOIN {uc_discounts_authors} dau ON n.uid=dau.author_id WHERE dau.discount_id=%d'; } $result = db_query($query, $discount->discount_id); while ($row = db_fetch_object($result)) { $product_ids[] = $row->nid; } return $product_ids; } return array(); } /** * Returns product_ids for discount. * Note: this function does not check filter_type so a discount with filter_type other than * FILTER_TYPE_PRODUCTS will return no values. * * @param $discount_id * @param $grouping * @param $exclude_all_products * * @return array of product ids (nids) */ function get_product_ids_for_discount($discount_id, $grouping, $exclude_all_products = FALSE) { $query = "SELECT product_id FROM {uc_discounts_products} WHERE discount_id = %d AND grouping = %d"; $args = array($discount_id, $grouping); if ($exclude_all_products) { $query .= ' AND product_id <> %d'; $args[] = ALL_PRODUCTS; } $result = db_query($query, $args); $ids = array(); while ($row = db_fetch_array($result)) { $ids[] = $row["product_id"]; } return $ids; } /** * Returns term_ids for discount. */ function get_term_ids_for_discount($discount_id, $grouping, $exclude_all_terms = FALSE) { $query = "SELECT term_id FROM {uc_discounts_terms} WHERE discount_id = %d AND grouping = %d"; $args = array($discount_id, $grouping); if ($exclude_all_products) { $query .= ' AND term_id <> %d'; $args[] = ALL_TERMS; } $result = db_query($query, $args); $ids = array(); while ($row = db_fetch_array($result)) { $ids[] = $row["term_id"]; } return $ids; } /** * Returns SKUs for discount. */ function get_skus_for_discount($discount_id, $grouping, $exclude_all_skus = FALSE) { $query = "SELECT sku FROM {uc_discounts_skus} WHERE discount_id = %d AND grouping = %d"; $args = array($discount_id, $grouping); if ($exclude_all_products) { $query .= ' AND sku <> "%s"'; $args[] = ALL_SKUS; } $result = db_query($query, $args); $ids = array(); while ($row = db_fetch_array($result)) { $ids[] = $row["sku"]; } return $ids; } /** * Returns Product Class names for discount. */ function get_classes_for_discount($discount_id, $grouping, $exclude_all_classes = FALSE) { $query = "SELECT class FROM {uc_discounts_classes} WHERE discount_id = %d AND grouping = %d"; $args = array($discount_id, $grouping); if ($exclude_all_products) { $query .= ' AND class <> "%s"'; $args[] = ALL_CLASSES; } $result = db_query($query, $args); $ids = array(); while ($row = db_fetch_array($result)) { $ids[] = $row["class"]; } return $ids; } /** * Returns author_ids for discount. */ function get_author_ids_for_discount($discount_id, $grouping, $exclude_all_authors = FALSE) { $query = "SELECT author_id FROM {uc_discounts_authors} WHERE discount_id = %d AND grouping = %d"; $args = array($discount_id, $grouping); if ($exclude_all_products) { $query .= ' AND author_id <> %d'; $args[] = ALL_AUTHORS; } $result = db_query($query, $args); $ids = array(); while ($row = db_fetch_array($result)) { $ids[] = $row["author_id"]; } return $ids; } /** * Returns role_ids for discount. */ function get_role_ids_for_discount($discount_id, $exclude_all_roles = FALSE) { $query = "SELECT role_id FROM {uc_discounts_roles} WHERE discount_id = %d"; $args = array($discount_id); if ($exclude_all_products) { $query .= ' AND role_id <> %d'; $args[] = ALL_ROLES; } $result = db_query($query, $args); $ids = array(); while ($row = db_fetch_array($result)) { $ids[] = $row["role_id"]; } return $ids; } /** * Deletes all uc_discounts_products rows for a discount. */ function uc_discounts_products_delete($discount_id) { $query = "DELETE FROM {uc_discounts_products} WHERE discount_id=%d"; db_query($query, $discount_id); } /** * Deletes all uc_discounts_terms rows for a discount. */ function uc_discounts_terms_delete($discount_id) { $query = "DELETE FROM {uc_discounts_terms} WHERE discount_id=%d"; db_query($query, $discount_id); } /** * Deletes all uc_discounts_skus rows for a discount. */ function uc_discounts_skus_delete($discount_id) { $query = "DELETE FROM {uc_discounts_skus} WHERE discount_id=%d"; db_query($query, $discount_id); } /** * Deletes all uc_discounts_classes rows for a discount. */ function uc_discounts_classes_delete($discount_id) { $query = "DELETE FROM {uc_discounts_classes} WHERE discount_id=%d"; db_query($query, $discount_id); } /** * Deletes all uc_discounts_authors rows for a discount. */ function uc_discounts_authors_delete($discount_id) { $query = "DELETE FROM {uc_discounts_authors} WHERE discount_id=%d"; db_query($query, $discount_id); } /** * Deletes all uc_discounts_roles rows for a discount. */ function uc_discounts_roles_delete($discount_id) { $query = "DELETE FROM {uc_discounts_roles} WHERE discount_id=%d"; db_query($query, $discount_id); } /** * Returns discounts for order. * Note: $order->uc_discounts_codes must be set * * @param $order Order to get discounts for * @param $errors Reference to array to add error messages to * @param $messages Reference to array to add success messages to * * @return array of discount objects */ function get_discounts_for_order($order, &$errors = NULL, &$warnings = NULL, &$messages = NULL) { // Product NIDS in cart => subtotal of individual item $order_product_id_subtotal_map = array(); // Product NIDS in cart => quantity of individual item $order_product_id_quantity_map = array(); // Product NIDS in cart $order_product_ids = array(); // Product NIDS in cart=> bool $order_product_ids_set = array(); // Product objects in cart $order_product_id_product_array_map = array(); $order_subtotal = 0; //Create IN string of product node IDs in order if (is_array($order->products) && !empty($order->products)) { foreach ($order->products as $product) { $nid = $product->nid; $order_product_ids_set[$nid] = TRUE; if (is_array($product->data) && !empty($product->data['kit_id'])) { $kit_id = $product->data['kit_id']; $order_product_ids_set[$kit_id] = TRUE; $kits[$kit_id]['product_qty'] += $product->qty; } uc_discounts_add_to_existing_map_number_value($order_product_id_subtotal_map, $nid, get_uc_price($product)); uc_discounts_add_to_existing_map_number_value($order_product_id_quantity_map, $nid, $product->qty); $a = $order_product_id_product_array_map[$nid]; if (!is_array($a)) { $a = array(); } $a[] = $product; $order_product_id_product_array_map[$nid] = $a; $order_subtotal += get_uc_price($product); } if (is_array($kits) && !empty($kits)) { foreach ($kits as $kit_id => $value) { $kit_node = node_load($kit_id); foreach ($kit_node->products as $product_in_kit) { $pik_nid = $product_in_kit->nid; foreach ($order->products as $key => $product) { if ($product->nid == $pik_nid && $product->data['kit_id'] == $kit_id) { $kits[$kit_id]['kit_qty'] = $product->qty / $product_in_kit->qty; break; } } } uc_discounts_add_to_existing_map_number_value($order_product_id_quantity_map, $kit_id, $kits[$kit_id]['kit_qty']); } } } // Populate product NID array with NIDs from the order $order_product_ids = array_keys($order_product_ids_set); $temp_product_ids = $order_product_ids; $temp_product_ids[] = ALL_PRODUCTS; $product_ids_clause = sprintf("d.filter_type<>%d OR dp.product_id IN(%s)", FILTER_TYPE_PRODUCTS, join(",", $temp_product_ids) ); //Create IN string of term TIDs in order $temp_term_ids = array(); $temp_term_ids[] = ALL_TERMS; if (is_array($order->products) && !empty($order->products)) { //Get terms for order's products $result = db_query("SELECT DISTINCT tid FROM {term_node} WHERE nid IN(%s)", join(",", $order_product_ids)); while ($row = db_fetch_array($result)) { $temp_term_ids[] = $row["tid"]; $order_term_ids[] = $row["tid"]; } } $term_ids_clause = sprintf("d.filter_type<>%d OR dt.term_id IN(%s)", FILTER_TYPE_TERMS, join(",", $temp_term_ids) ); //Create IN string of SKUs in order $temp_skus = array(); $temp_skus[] = "'". db_escape_string(ALL_SKUS) ."'"; if (is_array($order->products) && !empty($order->products)) { //Get SKUs for order's products $result = db_query("SELECT DISTINCT model FROM {uc_products} WHERE nid IN(%s)", join(",", $order_product_ids)); while ($row = db_fetch_array($result)) $temp_skus[] = "'". db_escape_string($row["model"]) ."'"; } $skus_clause = sprintf("d.filter_type<>%d OR ds.sku IN(%s)", FILTER_TYPE_SKUS, join(",", $temp_skus) ); //Create IN string of classes in order $temp_classes = array(); $temp_classes[] = "'". db_escape_string(ALL_CLASSES) ."'"; if (is_array($order->products) && !empty($order->products)) { //Get classes for order's products $result = db_query("SELECT DISTINCT type FROM {node} WHERE nid IN(%s)", join(",", $order_product_ids)); while ($row = db_fetch_array($result)) $temp_classes[] = "'". db_escape_string($row["type"]) ."'"; } $classes_clause = sprintf("d.filter_type<>%d OR dcl.class IN(%s)", FILTER_TYPE_CLASS, join(",", $temp_classes) ); //Create IN string of authors in order $temp_authors = array(); $temp_authors[] = "'". db_escape_string(ALL_AUTHORS) ."'"; if (is_array($order->products) && !empty($order->products)) { //Get authors for order's products $result = db_query("SELECT DISTINCT uid FROM {node} WHERE nid IN(%s)", join(",", $order_product_ids)); while ($row = db_fetch_array($result)) $temp_authors[] = "'". db_escape_string($row["uid"]) ."'"; } $authors_clause = sprintf("d.filter_type<>%d OR dau.author_id IN(%s)", FILTER_TYPE_AUTHORS, join(",", $temp_authors) ); //Create codes clause $escaped_codes_string = NULL; if (!empty($order->uc_discounts_codes)) { //Create IN string of product node IDs in order $escaped_codes = array(); foreach ($order->uc_discounts_codes as $code) { $escaped_codes[] = "'". db_escape_string($code) ."'"; } $escaped_codes_string = join(",", $escaped_codes); $codes_clause = sprintf(" OR d.discount_id IN( SELECT discount_id FROM {uc_discounts_codes} WHERE code IN(%s) )", $escaped_codes_string ); } else { $codes_clause = ""; } //Create roles clause $auth_rid = ($order->uid != 0) ? DRUPAL_AUTHENTICATED_RID : DRUPAL_ANONYMOUS_RID; $roles_clause = sprintf(" OR d.discount_id IN(SELECT dr.discount_id FROM {uc_discounts_roles} dr LEFT JOIN {users_roles} ur ON (dr.role_id=ur.rid AND ur.uid=%d) WHERE ur.uid IS NOT NULL OR dr.role_id=%d OR dr.role_id=%d)", $order->uid, ALL_ROLES, $auth_rid ); $grouping = DISCOUNT_FILTER_GROUPING_APPLICATION; //Add warnings for expired discounts with codes (if necessary) if (!empty($order->uc_discounts_codes)) { $query = sprintf("SELECT DISTINCT d.*, dc.code code FROM {uc_discounts} d LEFT JOIN {uc_discounts_products} dp ON d.discount_id=dp.discount_id AND dp.grouping = $grouping LEFT JOIN {uc_discounts_terms} dt ON d.discount_id=dt.discount_id AND dt.grouping = $grouping LEFT JOIN {uc_discounts_skus} ds ON d.discount_id=ds.discount_id AND ds.grouping = $grouping LEFT JOIN {uc_discounts_classes} dcl ON d.discount_id=dcl.discount_id AND dcl.grouping = $grouping LEFT JOIN {uc_discounts_authors} dau ON d.discount_id=dau.discount_id AND dau.grouping = $grouping LEFT JOIN {uc_discounts_roles} dr ON d.discount_id=dr.discount_id LEFT JOIN {uc_discounts_codes} dc ON d.discount_id=dc.discount_id WHERE dc.code IN(%s) AND (d.has_role_filter=0%s) AND (%s) AND (%s) AND (%s) AND (%s) AND (%s) AND (d.has_expiration<>0 AND d.expiration<=%d) AND (d.is_active=%d) ORDER BY weight", $escaped_codes_string, $roles_clause, $product_ids_clause, $term_ids_clause, $skus_clause, $classes_clause, $authors_clause, time(), IS_ACTIVE ); $result = db_query($query); while ($discount = db_fetch_object($result)) { $warnings[] = t('The discount for code "@code" has expired.', array("@code" => $discount->code) ); } } $query = sprintf("SELECT DISTINCT d.* FROM {uc_discounts} d LEFT JOIN {uc_discounts_products} dp ON d.discount_id=dp.discount_id AND dp.grouping = $grouping LEFT JOIN {uc_discounts_terms} dt ON d.discount_id=dt.discount_id AND dt.grouping = $grouping LEFT JOIN {uc_discounts_skus} ds ON d.discount_id=ds.discount_id AND ds.grouping = $grouping LEFT JOIN {uc_discounts_classes} dcl ON d.discount_id=dcl.discount_id AND dcl.grouping = $grouping LEFT JOIN {uc_discounts_authors} dau ON d.discount_id=dau.discount_id AND dau.grouping = $grouping WHERE (d.requires_code=0%s) AND (d.has_role_filter=0%s) AND (%s) AND (%s) AND (%s) AND (%s) AND (%s) AND (d.has_activation=0 OR d.activates_on<%d) AND (d.has_expiration=0 OR d.expiration>%d) AND (d.is_active=%d) ORDER BY weight", $codes_clause, $roles_clause, $product_ids_clause, $term_ids_clause, $skus_clause, $classes_clause, $authors_clause, time(), time(), IS_ACTIVE ); $result = db_query($query); $total_discount_amount = 0; $discounts = array(); // Appears to check if order qualifies for each discount then applies discount. Functionality should be separated, no? while ($discount = db_fetch_object($result)) { foreach (module_implements('uc_discount') as $module) { $function = $module . '_uc_discount'; $function('load', $discount, $order); } // In case the hook modified the discount if (!$discount->is_active) { continue; } //Get code for discount (if one exists) $discount->code = NULL; if (!empty($escaped_codes_string)) { $query = sprintf("SELECT code FROM {uc_discounts_codes} WHERE code IN(%s) AND discount_id=%d", $escaped_codes_string, $discount->discount_id ); $row = db_fetch_array(db_query($query)); if (!empty($row)) { $discount->code = $row["code"]; } } //The query handled valid codes and expiration, this block must: // check max uses (if applicable) // check if discount is being combined and can be combined // check if order qualifies (type, requires_single_product_to_qualify, required_products, can_be_combined_with_other_discounts) // determine number of times to apply discount //If this discount has a max uses amount, check max uses if ($discount->max_uses > 0) { $row = db_fetch_array(db_query("SELECT COUNT(*) as uses_count FROM {uc_discounts_uses} WHERE discount_id=%d", $discount->discount_id )); if ($row["uses_count"] >= $discount->max_uses) { //If this is a coded discount, add error message if (!is_null($warnings) && !is_null($discount->code)) { $warnings[] = t('The discount for code "@code" has reached its maximum number of uses.', array("@code" => $discount->code) ); } continue; } $discount->uses_count = $row["uses_count"]; } //If this discount has a max uses per user amount, check max uses per user if ($discount->max_uses_per_user > 0) { $row = db_fetch_array(db_query("SELECT COUNT(*) as user_uses_count FROM {uc_discounts_uses} WHERE discount_id=%d AND user_id=%d", $discount->discount_id, $order->uid )); if ($row["user_uses_count"] >= $discount->max_uses_per_user) { //If this is a coded discount, add warning message if (!is_null($warnings) && !is_null($discount->code)) { $warnings[] = t('The discount for code "@code" has reached its maximum number of uses.', array("@code" => $discount->code) ); } continue; } $discount->user_uses_count = $row["user_uses_count"]; } //If code exists and this discount has a max uses per code amount, check max uses per code if (!is_null($discount->code) && ($discount->max_uses_per_code > 0)) { $row = db_fetch_array(db_query("SELECT COUNT(*) as code_uses_count FROM {uc_discounts_uses} WHERE discount_id=%d AND code='%s'", $discount->discount_id, $discount->code )); if ($row["code_uses_count"] >= $discount->max_uses_per_code) { //Add warning message if (!is_null($warnings)) { $warnings[] = t('The discount code "@code" has reached its max number of uses.', array("@code" => $discount->code) ); } continue; } $discount->code_uses_count = $row["code_uses_count"]; } //If there are applied discounts, check if discount is being combined and can be combined if (count($discounts) > 0) { if (!$discount->can_be_combined_with_other_discounts) { //If this is a coded discount, add error message if (!is_null($warnings) && !is_null($discount->code)) { $warnings[] = t('The discount for code "@code" cannot be combined with other discounts.', array("@code" => $discount->code) ); } continue; } // Check if the first discount can't be combined if (!$discounts[0]->can_be_combined_with_other_discounts) { //If first discount was a coded discount, add error message (only print warning if both //discounts have codes) if (!is_null($warnings) && !empty($discounts[0]->code) && !is_null($discount->code)) { $warnings[] = t('The discount for code "@code" cannot be combined with other discounts.', array("@code" => $discounts[0]->code) ); } continue; } } //Check if order qualifies for this discount (check type, requires_single_product_to_qualify, required_products) //Get product IDs for determining discount application $discount_product_ids = get_product_ids_for_discount_object($discount); if (in_array(ALL_PRODUCTS, $discount_product_ids)) { $discount_product_ids = $order_product_ids; } // Get product IDs for determining discount qualification $required_product_ids = get_product_ids_for_discount_object($discount, DISCOUNT_FILTER_GROUPING_QUALIFICATION); $required_ids_in_order = array_intersect($required_product_ids, $order_product_ids); if (!empty($discount->code) && !empty($required_product_ids) && empty($required_ids_in_order)) { $warnings[] = t('The discount for code "@code" requires a product that is not in your cart.', array('@code' => $discount->code)); continue; } if ($discount->use_only_discounted_products_to_qualify) { $qualification_product_ids = $discount_product_ids; } elseif (!empty($required_product_ids)) { $qualification_product_ids = $required_product_ids; } else { $qualification_product_ids = $order_product_ids; } //Determine total qualifying amount of order (store in order_qualifying_amount) $order_qualifying_amount = 0; switch ($discount->qualifying_type) { case QUALIFYING_TYPE_MINIMUM_PRICE: //Determine the total subtotal of discount's products foreach ($qualification_product_ids as $product_id) { if (isset($order_product_id_subtotal_map[$product_id])) { if ($discount->requires_single_product_to_qualify) { if ($order_product_id_subtotal_map[$product_id] >= $order->qualifying_amount) { //In this case, $order_qualifying amount should be the sum of //prices of products that both qualify and meet the minimum //qualification amount based on their individual price. $order_qualifying_amount += $order_product_id_subtotal_map[$product_id]; } } else { $order_qualifying_amount += $order_product_id_subtotal_map[$product_id]; } } } //Subtract already discounted amount $order_qualifying_amount -= $total_discount_amount; break; case QUALIFYING_TYPE_MINIMUM_QUANTITY: //Determine the total quantity of discount's products foreach ($qualification_product_ids as $product_id) { if (isset($order_product_id_quantity_map[$product_id])) { if ($discount->requires_single_product_to_qualify) { if ($order_product_id_quantity_map[$product_id] >= $discount->qualifying_amount) { //In this case, $order_qualifying amount should be the sum of products that both qualify and meet the minimum qualification amount based on their quantity. $order_qualifying_amount += $order_product_id_quantity_map[$product_id]; } } else { $order_qualifying_amount += $order_product_id_quantity_map[$product_id]; } } } break; } //If order does not qualify for this discount if ($order_qualifying_amount < $discount->qualifying_amount) { //If this is a coded discount, add warning message if (!is_null($warnings) && !is_null($discount->code)) { switch ($discount->qualifying_type) { case QUALIFYING_TYPE_MINIMUM_PRICE: $qualifying_amount = uc_currency_format($discount->qualifying_amount); $warnings[] = t('The discount for code "@code" requires a minimum amount of @qualifying_amount to qualify.', array("@code" => $discount->code, "@qualifying_amount" => $qualifying_amount) ); break; case QUALIFYING_TYPE_MINIMUM_QUANTITY: $warnings[] = t('The discount for code "@code" requires a minimum quantity of @qualifying_amount to qualify.', array("@code" => $discount->code, "@qualifying_amount" => $discount->qualifying_amount) ); break; } } continue; } //If this discount has a maximum qualifying amount and the order exceeds it if ($discount->has_qualifying_amount_max && ($order_qualifying_amount > $discount->qualifying_amount_max)) { //If this is a coded discount, add error message if (!is_null($warnings) && !is_null($discount->code)) { $qualifying_amount_max = uc_currency_format($discount->qualifying_amount_max); switch ($discount->qualifying_type) { case QUALIFYING_TYPE_MINIMUM_PRICE: $warnings[] = t('The discount for code "@code" cannot exceed the price of @qualifying_amount_max to qualify.', array("@code" => $discount->code, "@qualifying_amount_max" => $qualifying_amount_max) ); break; case QUALIFYING_TYPE_MINIMUM_QUANTITY: $warnings[] = t('The discount for code "@code" cannot exceed the quantity of @qualifying_amount_max to qualify.', array("@code" => $discount->code, "@qualifying_amount_max" => $discount->qualifying_amount_max) ); break; } } continue; } //Get product IDs in order that are in discount $order_and_discount_product_ids = array_intersect($discount_product_ids, $order_product_ids); //Create array of product objects in cart to which this discount gets applied. $order_and_discount_products = array(); foreach ($order_and_discount_product_ids as $product_id) { if (array_key_exists($product_id, $order_product_id_product_array_map)) { $order_and_discount_products = array_merge($order_and_discount_products, $order_product_id_product_array_map[$product_id]); } } // Amount of products to which discounts get applied $discount_products_amount = 0; // Quantity of products to which discounts get applied $discount_products_qty = 0; foreach ($order_and_discount_products as $product) { $discount_products_qty += $product->qty; $discount_products_amount += $product->qty * $product->price; } // Determine number of times to apply discount, by default once for every qualifying product $discount->times_applied = $discount_products_qty; // See if it should be limited based on number of required products in the cart if ($discount->limit_max_times_applied && !empty($required_product_ids)) { $times = 0; foreach ($required_product_ids as $id) { if (isset($order_product_id_quantity_map[$id])) { $times += $order_product_id_quantity_map[$id]; } } $discount->times_applied = min($discount->times_applied, $times); } // See if we need to limit the number of applications with a hard cap if ($discount->max_times_applied != 0) { $discount->times_applied = min($discount->times_applied, $discount->max_times_applied); } // If uc_vat module exists, add the rate type and rate name to discount SCUK if (module_exists('uc_vat') && module_exists('uc_taxes')) { uc_discounts_add_discount_rate_id($discount, $order_and_discount_product_ids); } switch ($discount->discount_type) { case DISCOUNT_TYPE_FREE_ITEMS: //The variable discount_amount is the monitary amount of discount $discount_amount = 0; //The variable free_items_remaining is the [max] number of free items for the order $free_items_remaining = $discount->discount_amount * $discount->times_applied; //Loop until all free items have been applied or there are no more products to //discount (discount cheapest first) while ($free_items_remaining > 0) { //Determine cheapest remaining qualifying item $cheapest_product = NULL; foreach ($order_and_discount_products as $product) { //If this product has been fully discounted, continue if ($product->uc_discounts_is_fully_discounted) { continue; } //If no current cheapest product exists, use this product if (is_null($cheapest_product)) { $cheapest_product = $product; } else { //If this product is cheaper than the current cheapest product, //use this product instead if ($product->price < $cheapest_product->price) { $cheapest_product = $product; } } } //If no cheapest product could be found, there are no more products to //discount, break if (is_null($cheapest_product)) break; //Discount up to the lesser of cheapest product quantity and free_items_remaining $discount_count = min($cheapest_product->qty, $free_items_remaining); //Add current discount amount to running total $discount_amount += $discount_count * $cheapest_product->price; //Mark item fully discounted $cheapest_product->uc_discounts_is_fully_discounted = TRUE; $free_items_remaining -= $discount_count; } $discount->amount = $discount_amount; break; case DISCOUNT_TYPE_PERCENTAGE_OFF_PER_QUALIFYING_ITEM: $discount->amount = ($discount_products_amount / $discount_products_qty) * $discount->discount_amount * $discount->times_applied; break; case DISCOUNT_TYPE_PERCENTAGE_OFF: // This is so complicated because we need to ensure only qualifying // products get discounted and no product is discounted more than 100% // Always apply once since it applies to the whole order $discount->times_applied = 1; //If this discount uses all products and previous discount is: // same weight as this discount // percentage off // products of discounts must match //discount using same subtotal as last discount if (count($discounts) > 0) { $last_discount = $discounts[count($discounts) - 1]; if ($last_discount->weight == $discount->weight && $last_discount->discount_type == DISCOUNT_TYPE_PERCENTAGE_OFF) { //Last discount's and this discount's products must match exactly $are_equal = TRUE; $last_discount_product_ids = get_product_ids_for_discount_object($last_discount); $this_discount_product_ids = get_product_ids_for_discount_object($discount); //If both contain "all products" they are equal if (in_array(ALL_PRODUCTS, $last_discount_product_ids) && in_array(ALL_PRODUCTS, $this_discount_product_ids)) { $are_equal = TRUE; } //Otherwise check arrays for equality else { foreach ($this_discount_product_ids as $product_id) { if (!in_array($product_id, $last_discount_product_ids)) { $are_equal = FALSE; break; } } if ($are_equal) { foreach ($last_discount_product_ids as $product_id) { if (!in_array($product_id, $this_discount_product_ids)) { $are_equal = FALSE; break; } } } } if ($are_equal) { //($last_discount->amount / $last_discount->discount_amount) == last discount's subtotal $local_order_subtotal = ($last_discount->amount / $last_discount->discount_amount); $discount->amount = $local_order_subtotal * $discount->discount_amount; break; } } } //Start patch from lutegrass: //This fixes the problem where a percent discount does not apply to all products //(but doesn't fix the problem where the products being discounted have already been discounted //in full, or the case where the cart consists only of the products included in this discount) // Get qualifying products -- ignore "all products" selection $discount_product_ids = get_product_ids_for_discount_object($discount, DISCOUNT_FILTER_GROUPING_APPLICATION, TRUE); // Do we have any products /* DUNXX * discount amount for text, that includes VAT * discount amount, that excludes VAT for table. * Is uc_price (context revision altered/original) including VAT through hook_uc_price_handler? */ if (count($discount_product_ids) > 0) { $discounted_products_amount = 0; $discounted_products_total = 0; foreach ($order_and_discount_products as $product) { $discounted_products_total += get_uc_price($product); $discounted_products_amount += get_uc_price($product, 'original'); } $discount->amount = $discounted_products_amount * $discount->discount_amount; $discount->total = $discounted_products_total * $discount->discount_amount; } else { $discount->amount = max($order_subtotal - $total_discount_amount, 0) * $discount->discount_amount; } //End patch from lutegrass break; case DISCOUNT_TYPE_FIXED_AMOUNT_OFF: // Always apply once since it applies to the whole order $discount->times_applied = 1; $discount->amount = $discount->discount_amount; break; case DISCOUNT_TYPE_FIXED_AMOUNT_OFF_PER_QUALIFYING_ITEM: $discount->amount = $discount->discount_amount * $discount->times_applied; break; } if (!is_null($messages)) { $options = array("@short_description" => $discount->short_description, "@code" => $discount->code, "@times_applied" => $discount->times_applied, "@discount_total" => uc_currency_format($discount->total), "@time_string" => $time_string, ); if (!is_null($discount->code)) { if (empty($discount->amount)) { $messages[] = t("The discount, '@short_description', with code '@code' was applied.", $options); } elseif ($discount->times_applied == 1) { $messages[] = t("The discount, '@short_description', with code '@code' was applied for a discount of @discount_total", $options); } else { $messages[] = t("The discount, '@short_description', with code '@code' was applied @times_applied times for a discount of @discount_total", $options); } } else { if (empty($discount->amount)) { $messages[] = t("The discount, '@short_description' was applied.", $options); } elseif ($discount->times_applied == 1) { $messages[] = t("The discount, '@short_description', was applied for a discount of @discount_total", $options); } else { $messages[] = t("The discount, '@short_description', was applied @times_applied times for a discount of @discount_total", $options); } } } //Round the discount to two places $discount->amount = round($discount->amount, 2); //Add this discount's amount to running total $total_discount_amount += $discount->amount; //Add this discount to list of discounts applied to order $discounts[] = $discount; } // If no discount array was filled in, means that the discount was not found in the database if (count($discounts) == 0 && !empty($order->uc_discounts_codes)) { $warnings[] = t('Coupon does not exist or is not valid.'); } // end of db fetch while loop return $discounts; } /** * Helper function that gets the discount amount for the order * @return int * */ function uc_discounts_get_discount_amount_for_order($op) { global $user; $order = uc_order_load($_SESSION["cart_order"]); if (!$order) { $items = uc_cart_get_contents(); $order = new stdClass(); $order->uid = $user->uid; $order->products = $items; } $errors = array(); $warnings = array(); $messages = array(); $discounts = get_discounts_for_order($order, $errors, $warnings, $messages); $total_discount_amount = 0; if (is_array($discounts)) { foreach ($discounts as $discount) { if (!$discount->requires_code || $op == 'review') { $total_discount_amount += $discount->amount; } } } return $total_discount_amount; } /** * Helper function that gets the codeless discounted price of a product * @param $product A node product * @param $return_null Return NULL for the price if there are no discounts. Otherwise it returns the sell_price */ function uc_discounts_get_discounted_price_for_product($product, $return_null = TRUE) { $discounts = get_codeless_discounts_for_product_and_quantity($product, 1); if (empty($discounts)) { return $return_null ? NULL : $product->sell_price; } $total_discount_amount = 0; foreach ($discounts as $discount) { $total_discount_amount += $discount->amount; } return $product->sell_price - $total_discount_amount; } /** * Returns all codeless discounts for product. */ function get_codeless_discounts_for_product($product, $sort_column = "weight", $is_ascending_sort = TRUE) { return get_codeless_discounts_for_product_and_quantity($product, NULL, $sort_column, $is_ascending_sort); } /** * Returns all codeless discounts for product when specified quantity is purchased. * * @param $product_id Node ID for product */ function get_codeless_discounts_for_product_and_quantity($product, $quantity = NULL, $sort_column = "weight", $is_ascending_sort = TRUE) { if (is_null($product) || !$product) { return array(); } //If quantity was specified if (!is_null($quantity)) { global $user; //Create phony order and get discounts for order $product->price = $product->sell_price; $product->qty = $quantity; $order = new stdClass(); $order->uid = $user->uid; $order->products = array($product); return get_discounts_for_order($order); } //Otherwise make special query //Get terms for product $term_ids = array(); $term_ids[] = ALL_TERMS; $result = db_query("SELECT DISTINCT tid FROM {term_node} WHERE nid=%d", $product->nid); while ($row = db_fetch_array($result)) $term_ids[] = $row["tid"]; //Get SKUs for product $skus = array(); $skus[] = "'". db_escape_string(ALL_SKUS) ."'"; $result = db_query("SELECT DISTINCT model FROM {uc_products} WHERE nid=%d", $product->nid); while ($row = db_fetch_array($result)) $skus[] = "'". db_escape_string($row["model"]) ."'"; //Get classes for product $classes = array(); $classes[] = "'". db_escape_string(ALL_CLASSES) ."'"; $result = db_query("SELECT DISTINCT type FROM {node} WHERE nid=%d", $product->nid); while ($row = db_fetch_array($result)) $classes[] = "'". db_escape_string($row["type"]) ."'"; //Get uids for product $authors = array(); $authors[] = "'". db_escape_string(ALL_AUTHORS) ."'"; $result = db_query("SELECT DISTINCT uid FROM {node} WHERE nid=%d", $product->nid); while ($row = db_fetch_array($result)) $authors[] = "'". db_escape_string($row["uid"]) ."'"; //Create roles clause global $user; $auth_rid = ($user->uid != 0) ? DRUPAL_AUTHENTICATED_RID : DRUPAL_ANONYMOUS_RID; $roles_clause = sprintf(" OR d.discount_id IN(SELECT dr.discount_id FROM {uc_discounts_roles} dr" . ", {users_roles} ur WHERE (dr.role_id=ur.rid AND ur.uid=%d) OR dr.role_id=%d OR dr.role_id=%d)", $user->uid, ALL_ROLES, $auth_rid ); $product_ids = array(ALL_PRODUCTS, $product->nid); $product_ids_clause = sprintf("d.filter_type<>%d OR dp.product_id IN(%s)", FILTER_TYPE_PRODUCTS, join(",", $product_ids) ); $term_ids_clause = sprintf("d.filter_type<>%d OR dt.term_id IN(%s)", FILTER_TYPE_TERMS, join(",", $term_ids) ); $skus_clause = sprintf("d.filter_type<>%d OR ds.sku IN(%s)", FILTER_TYPE_SKUS, join(",", $skus) ); $classes_clause = sprintf("d.filter_type<>%d OR dcl.class IN(%s)", FILTER_TYPE_CLASS, join(",", $classes) ); $authors_clause = sprintf("d.filter_type<>%d OR dau.author_id IN(%s)", FILTER_TYPE_AUTHORS, join(",", $authors) ); $sort_order_string = (is_ascending_sort) ? "ASC" : "DESC"; $grouping = DISCOUNT_FILTER_GROUPING_APPLICATION; $query = sprintf("SELECT d.* FROM {uc_discounts} d LEFT JOIN {uc_discounts_products} dp ON d.discount_id = dp.discount_id AND dp.grouping = $grouping LEFT JOIN {uc_discounts_terms} dt ON d.discount_id = dt.discount_id AND dt.grouping = $grouping LEFT JOIN {uc_discounts_skus} ds ON d.discount_id = ds.discount_id AND ds.grouping = $grouping LEFT JOIN {uc_discounts_classes} dcl ON d.discount_id = dcl.discount_id AND dcl.grouping = $grouping LEFT JOIN {uc_discounts_authors} dau ON d.discount_id = dau.discount_id AND dau.grouping = $grouping WHERE d.requires_code = 0 AND (d.has_role_filter = 0%s) AND (%s) AND (%s) AND (%s) AND (%s) AND (%s) AND (d.has_activation = 0 OR d.activates_on < %d) AND (d.has_expiration = 0 OR d.expiration > %d) AND (d.is_active = %d) ORDER BY d.%s %s", $roles_clause, $product_ids_clause, $term_ids_clause, $skus_clause, $classes_clause, $authors_clause, time(), time(), IS_ACTIVE, $sort_column, $sort_order_string ); $result = db_query($query); $discounts = array(); while ($discount = db_fetch_object($result)) { $discounts[] = $discount; } return $discounts; } /** * Deletes all uc_discounts_uses rows for a discount. */ function uc_discounts_uses_delete_for_discount($discount_id) { $query = "DELETE FROM {uc_discounts_uses} WHERE discount_id=%d"; db_query($query, $discount_id); } /** * Deletes all uc_discounts_uses rows for an order. */ function uc_discounts_uses_delete_for_order($order_id) { $query = "DELETE FROM {uc_discounts_uses} WHERE order_id=%d"; db_query($query, $order_id); } /** * Records uses of a discount for an order */ function uc_discounts_uses_save_for_order($order) { $discounts = get_discounts_for_order($order); // Delete existing uses for order uc_discounts_uses_delete_for_order($order->order_id); // Insert uses (for best results use discounts returned by call to get_discounts_for_order) foreach ($discounts as $discount) { $code = (!empty($discount->code)) ? $discount->code : ''; $times_applied = (is_numeric($discount->times_applied)) ? $discount->times_applied : 1; $amount = (is_numeric($discount->amount)) ? $discount->amount : 0; $discount_use = new stdClass(); $discount_use->discount_id = $discount->discount_id; $discount_use->user_id = $order->uid; $discount_use->order_id = $order->order_id; $discount_use->code = $code; $discount_use->times_applied = $times_applied; $discount_use->amount = $amount; $discount_use->insert_timestamp = time(); drupal_write_record('uc_discounts_uses', $discount_use); } } /** * Returns order codes for order. */ function uc_discounts_get_codes_for_order($order_id) { return uc_discounts_codes_to_array(db_result(db_query('SELECT codes FROM {uc_discounts_order_codes} WHERE order_id = %d', $order_id))); } /** * Applies the discounts to an order by creating the necessary line items. * @param $order The order object with any codes set in the uc_discounts_code property * @param $compare_to_existing Compare new discounts to existing order discounts and fail if they have changed? * @param $save_uses Save the discounts to the uses table? If not done here it will need to be done when the order is successfully created. * @return array An array with the keys: 'success' => @bool, 'message' => @str */ function uc_discounts_apply($order, $save_uses = TRUE, $compare_to_existing = TRUE) { // Store existing discount amounts $existing_discount_amounts = array(); foreach (get_existing_discount_line_items($order) as $line_item) { $existing_discount_amounts[] = uc_currency_format($line_item["amount"]); } // Regenerate discount amounts $errors = array(); $warnings = array(); $messages = array(); $discounts = get_discounts_for_order($order, $errors, $warnings, $messages); foreach ($warnings as $warning) { drupal_set_message($warning, 'error'); } // If there were errors, print and return FALSE if (!empty($errors)) { uc_order_log_changes($order->order_id, $errors); foreach ($errors as $error) { drupal_set_message($error, 'error'); } return array('success' => FALSE, 'message' => t('Discounts have changed. Please review your cart and continue checkout.')); } // Add discount line items to order add_discount_line_items_to_order($order, $discounts); $new_discount_amounts = array(); foreach ($order->uc_discounts_line_items as $line_item) { $new_discount_amounts[] = uc_currency_format($line_item["amount"]); } if ($compare_to_existing) { $discount_intersection = array_intersect($existing_discount_amounts, $new_discount_amounts); if (count($discount_intersection) != count($existing_discount_amounts)) { // Save new discount line items $order->uc_discounts_line_items_need_updating = TRUE; uc_discounts_order('save', $order, NULL); return array('success' => FALSE, 'message' => t('Discounts have changed. Please review your cart and continue checkout.')); } } else { $order->uc_discounts_line_items_need_updating = TRUE; uc_discounts_order('save', $order, NULL); } if ($save_uses) { uc_discounts_uses_save_for_order($order); } uc_order_log_changes($order->order_id, $messages); return array('success' => TRUE, 'message' => t('Discount code(s) applied to order.')); } /** * Returns existing discounts line items for order. */ function get_existing_discount_line_items($order) { if (is_array($order->line_items)) { $existing_line_items = $order->line_items; } else { $existing_line_items = uc_order_load_line_items($order, TRUE); } $line_items = array(); foreach ($existing_line_items as $line_item) { //If line item type is LINE_ITEM_KEY_NAME, add it to array // Allow LINE_ITEM_KEY_NAME suffixes SCUK if (strpos($line_item["type"], LINE_ITEM_KEY_NAME) !== false) { $line_items[] = $line_item; } } return $line_items; } /** * Deletes all uc_discounts_order_codes rows for an order. */ function uc_discounts_order_codes_delete($order_id) { $query = "DELETE FROM {uc_discounts_order_codes} WHERE order_id=%d"; db_query($query, $order_id); } /////////////////////////////////////////////////////////////////// //Misc. helper functions /////////////////////////////////////////////////////////////////// /** * Returns a string list of codes into an array of codes */ function uc_discounts_codes_to_array($codes_string) { $codes = array(); $raw_codes = explode("\n", $codes_string); foreach ($raw_codes as $raw_code) { $code = trim($raw_code); if (!empty($code)) { $codes[] = $code; } } return $codes; } /** * Create a codes string from passed codes array. * Note: returns "" if passed array is null */ function uc_discounts_codes_to_str($codes) { return implode("\n", (array) $codes); } /** * Returns an array of qualifying types with descriptions. */ function qualifying_type_options() { static $options = NULL; if (is_null($options)) { $options = array(QUALIFYING_TYPE_MINIMUM_PRICE => t("Minimum price"), QUALIFYING_TYPE_MINIMUM_QUANTITY => t("Minimum quantity"), ); } return $options; } function qualifying_type_name($qualifying_type) { $options = qualifying_type_options(); return $options[$qualifying_type]; } /** * Returns an array of discount types with descriptions. */ function discount_type_options() { static $options = NULL; if (is_null($options)) { $options = array( DISCOUNT_TYPE_PERCENTAGE_OFF => t("Percentage off"), DISCOUNT_TYPE_PERCENTAGE_OFF_PER_QUALIFYING_ITEM => t("Percentage off per qualifying item"), DISCOUNT_TYPE_FIXED_AMOUNT_OFF => t("Fixed amount off"), DISCOUNT_TYPE_FIXED_AMOUNT_OFF_PER_QUALIFYING_ITEM => t("Fixed amount off per qualifying item"), DISCOUNT_TYPE_FREE_ITEMS => t("Free items"), ); } return $options; } function discount_type_name($discount_type) { $options = discount_type_options(); return $options[$discount_type]; } function discount_amount_formatted($discount) { if (in_array($discount->discount_type, array(DISCOUNT_TYPE_PERCENTAGE_OFF, DISCOUNT_TYPE_PERCENTAGE_OFF_PER_QUALIFYING_ITEM))) { return ($discount->discount_amount * 100) . '%'; } elseif (in_array($discount->discount_type, array(DISCOUNT_TYPE_FIXED_AMOUNT_OFF, DISCOUNT_TYPE_FIXED_AMOUNT_OFF_PER_QUALIFYING_ITEM))) { return uc_currency_format($discount->discount_amount); } else { return $discount->discount_amount; } } function uc_discounts_add_to_existing_map_number_value(&$a, $key, $value) { $a[$key] = (array_key_exists($key, $a)) ? $a[$key] + $value : $value; } function uc_discounts_views_api() { return array('api' => 2); } /** SCUK * Returns an array with taxed product types as keys and * an array with tax rate id and tax rate name as values */ function uc_discounts_get_taxes_array() { $rates = uc_taxes_rate_load(); $taxes = array(); foreach ($rates as $rate) { foreach ($rate->taxed_product_types as $type) { // The tax type per product type (class) must be unique! $taxes[$type] = array( "id" => $rate->id, "name" => $rate->name, ); } } return $taxes; } /** SCUK * Return an array with the cart products nids as keys and the product type (class) as values */ function uc_discounts_get_cart_nids() { // Get the cart_id $cid = uc_cart_get_id(FALSE); // If we didn't get a cid, return empty. if (!$cid) { return array(); } $result = db_query("SELECT n.nid, n.type FROM {node} n INNER JOIN {uc_cart_products} c ON n.nid = c.nid WHERE c.cart_id = '%s'", $cid); $cart_products_nids = array(); while ($item = db_fetch_object($result)) { $cart_products_nids[$item->nid] = $item->type; } return $cart_products_nids; } /** SCUK * Add rate type and rate name to discount * Warning: Products that qualifying for this discount must have the same tax rate, * if mixed the rate applied to discount will be random and the calculations will be wrong */ function uc_discounts_add_discount_rate_id(&$discount, $products_with_discount_ids) { $taxes = uc_discounts_get_taxes_array(); $products_in_cart = uc_discounts_get_cart_nids(); foreach ($products_with_discount_ids as $product) { // Check if product nid exists in cart if (array_key_exists($product, $products_in_cart)) { // Check if product type (class) exists in taxes if (array_key_exists($products_in_cart[$product],$taxes)) { // Get the tax rate type $discount->rate_type = $taxes[$products_in_cart[$product]]["id"]; // and the tax rate name $discount->rate_name = $taxes[$products_in_cart[$product]]["name"]; } } } }