You are here

function uc_coupon_calculate_discounts in Ubercart Discount Coupons 7.3

Same name and namespace in other branches
  1. 6 uc_coupon.module \uc_coupon_calculate_discounts()
  2. 7.2 uc_coupon.module \uc_coupon_calculate_discounts()

Find items that a coupon will apply to and calculate the discounts.

Parameters

$coupon: A coupon object to apply, or a coupon code as a string.

$order: The order object to which the coupon should be applied.

Return value

An array of discounts.

3 calls to uc_coupon_calculate_discounts()
uc_coupon_get_order_coupons in ./uc_coupon.module
Gets the fully validated coupon objects that have been applied to this order.
uc_coupon_recurring_recurring_renewal_pending in uc_coupon_recurring/uc_coupon_recurring.module
Implements hook_recurring_renewal_pending().
uc_coupon_validate in ./uc_coupon.module
Validate a coupon, and optionally calculate the order discount.

File

./uc_coupon.module, line 854
Provides discount codes and gift certificates for Ubercart.

Code

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;
}