You are here

commerce_discount.rules.inc in Commerce Discount 7

Rules integration for the Commerce Discount module.

File

commerce_discount.rules.inc
View source
<?php

/**
 * @file
 * Rules integration for the Commerce Discount module.
 */

/**
 * Implements hook_rules_condition_info().
 */
function commerce_discount_rules_condition_info() {
  $inline_conditions = inline_conditions_get_info();
  $conditions = array();

  // Max usage per person.
  $conditions['commerce_discount_usage_max_usage_per_person'] = array(
    'label' => t('Max usage per person'),
    'parameter' => array(
      'order' => array(
        'type' => 'commerce_order',
        'label' => t('Order'),
      ),
      'commerce_discount' => array(
        'label' => t('Commerce Discount'),
        'type' => 'token',
        'options list' => 'commerce_discount_entity_list',
      ),
      'usage' => array(
        'type' => 'integer',
        'label' => t('Maximum usage per customer'),
        'description' => t('Enter the maximum number of times a specific person (as identified by email) may use this discount. Leave blank for unlimited.'),
      ),
    ),
    'group' => t('Commerce Discount'),
  );

  // Max usage.
  $conditions['commerce_discount_usage_max_usage'] = array(
    'label' => t('Max usage'),
    'parameter' => array(
      'order' => array(
        'type' => 'commerce_order',
        'label' => t('Order'),
      ),
      'commerce_discount' => array(
        'label' => t('Commerce Discount'),
        'type' => 'token',
        'options list' => 'commerce_discount_entity_list',
      ),
      'usage' => array(
        'type' => 'integer',
        'label' => t('Maximum overall usage'),
        'description' => t('Enter the maximum number of times this discount may be used on the site, by anyone. Leave blank for unlimited.'),
      ),
    ),
    'group' => t('Commerce Discount'),
  );
  $conditions['commerce_discount_date_condition'] = array(
    'label' => t('Check discount dates'),
    'group' => t('Commerce Discount'),
    'parameter' => array(
      'commerce_discount' => array(
        'label' => t('Commerce Discount'),
        'type' => 'token',
        'options list' => 'commerce_discount_entity_list',
      ),
    ),
    'base' => 'commerce_discount_date_condition',
  );
  $conditions['commerce_discount_compatibility_check'] = array(
    'label' => t('Check discount compatibility at the order level'),
    'parameter' => array(
      'commerce_order' => array(
        'type' => 'commerce_order',
        'label' => t('Order'),
        'description' => t('The order the discount would be applied to.'),
        'wrapped' => TRUE,
      ),
      'commerce_discount' => array(
        'type' => 'token',
        'label' => t('Discount'),
        'description' => t('A discount with a compatibility strategy that requires evaluation.'),
        'options list' => 'commerce_discount_entity_list',
      ),
    ),
    'group' => t('Commerce Discount'),
  );
  $conditions['commerce_discount_line_item_compatibility_check'] = array(
    'label' => t('Check discount compatibility at the line item level'),
    'parameter' => array(
      'commerce_line_item' => array(
        'type' => 'commerce_line_item',
        'label' => t('Line item'),
        'description' => t('The line item the discount would be applied to.'),
        'wrapped' => TRUE,
      ),
      'commerce_discount' => array(
        'type' => 'token',
        'label' => t('Discount'),
        'description' => t('A discount with a compatibility strategy that requires evaluation.'),
        'options list' => 'commerce_discount_entity_list',
      ),
    ),
    'group' => t('Commerce Discount'),
  );
  if (module_exists('commerce_order')) {
    $conditions['commerce_order_compare_order_amount'] = array(
      'label' => t('Order amount comparison'),
      'parameter' => array(
        'commerce_order' => array(
          'type' => 'commerce_order',
          'label' => t('Order'),
          'description' => t('The order.'),
          'wrapped' => TRUE,
        ),
        'operator' => array(
          'type' => 'text',
          'label' => t('Operator'),
          'description' => t('The operator used with the order amount value below.'),
          'default value' => '>=',
          'options list' => '_commerce_discount_operator_options',
        ),
        'total' => array(
          'type' => 'commerce_price',
          'label' => t('Order amount'),
          'default value' => '',
          'description' => t('The value to compare against the passed order amount.'),
        ),
        'line_item_types' => array(
          'type' => 'list<text>',
          'label' => t('Line item types'),
          'description' => t('Select the line item types that should be included in the order total amount. Discount created line items are always excluded.'),
          'default value' => commerce_order_compare_order_amount_options_default(),
          'options list' => 'commerce_order_compare_order_amount_options_list',
          'optional' => TRUE,
        ),
      ),
      'module' => 'commerce_discount',
      'group' => t('Commerce Discount'),
      'callbacks' => array(
        'execute' => $inline_conditions['commerce_order_compare_order_amount']['callbacks']['build'],
      ),
    );
    $conditions['commerce_order_has_owner'] = array(
      'label' => t('Order owner'),
      'parameter' => array(
        'commerce_order' => array(
          'type' => 'commerce_order',
          'label' => t('Order'),
          'description' => t('The order.'),
          'wrapped' => TRUE,
        ),
        'account' => array(
          'type' => 'user',
          'label' => t('User'),
          'description' => t('User account.'),
        ),
      ),
      'module' => 'commerce_discount',
      'group' => t('Commerce Discount'),
      'callbacks' => array(
        'execute' => $inline_conditions['commerce_order_has_owner']['callbacks']['build'],
      ),
    );
    $conditions['commerce_order_contains_products'] = array(
      'label' => t('Order contains products'),
      'parameter' => array(
        'commerce_order' => array(
          'type' => 'commerce_order',
          'label' => t('Order'),
          'description' => t('The order.'),
          'wrapped' => TRUE,
        ),
        'products' => array(
          'type' => 'text',
          'label' => t('Product SKU(s)'),
          'description' => t('Products SKU to look for on the order. Enter a comma-separated list of product SKU(s).'),
        ),
        'operator' => array(
          'type' => 'text',
          'label' => t('Operator'),
          'description' => t('The comparison type used with the SKUs above.'),
          'default value' => 'any',
          'options list' => '_commerce_discount_rules_product_match_options',
          'optional' => TRUE,
        ),
      ),
      'module' => 'commerce_discount',
      'group' => t('Commerce Discount'),
      'callbacks' => array(
        'execute' => $inline_conditions['commerce_order_contains_products']['callbacks']['build'],
      ),
    );
    $conditions['commerce_order_has_specific_quantity_products'] = array(
      'label' => t('Order has a specific quantity of products'),
      'parameter' => array(
        'commerce_order' => array(
          'type' => 'commerce_order',
          'label' => t('Order'),
          'description' => t('The order.'),
          'wrapped' => TRUE,
        ),
        'products' => array(
          'type' => 'text',
          'label' => t('Product SKU(s)'),
          'description' => t('Products SKU to look for on the order. Enter a comma-separated list of product SKU(s).'),
        ),
        'operator' => array(
          'type' => 'text',
          'label' => t('Operator'),
          'description' => t('The operator used with the product quantity value below.'),
          'default value' => '>=',
          'options list' => '_commerce_discount_operator_options',
        ),
        'quantity' => array(
          'type' => 'integer',
          'label' => t('Quantity'),
          'description' => t('Quantity value to be compared against each selected product(s).'),
        ),
      ),
      'module' => 'commerce_discount',
      'group' => t('Commerce Discount'),
      'callbacks' => array(
        'execute' => $inline_conditions['commerce_order_has_specific_quantity_products']['callbacks']['build'],
      ),
    );
  }
  if (module_exists('commerce_product')) {
    $conditions['commerce_product_contains_products'] = array(
      'label' => t('Line item contains a specific product'),
      'parameter' => array(
        'commerce_line_item' => array(
          'type' => 'commerce_line_item',
          'label' => t('Line item'),
          'description' => t('The line item.'),
          'wrapped' => TRUE,
        ),
        'sku' => array(
          'type' => 'text',
          'label' => t('SKU'),
          'description' => t('Enter a comma-separated list of product SKU(s) to compare against the passed product line item.'),
        ),
      ),
      'module' => 'commerce_discount',
      'group' => t('Commerce Discount'),
      'callbacks' => array(
        'execute' => $inline_conditions['commerce_product_contains_products']['callbacks']['build'],
      ),
    );
    $conditions['commerce_product_has_type'] = array(
      'label' => t('Line item contains a specific product type'),
      'parameter' => array(
        'commerce_line_item' => array(
          'type' => 'commerce_line_item',
          'label' => t('Line item'),
          'description' => t('The line item.'),
          'wrapped' => TRUE,
        ),
        'type' => array(
          'type' => 'text',
          'label' => t('Type'),
          'description' => t('Enter the product type to compare against the passed product line item.'),
          'options list' => 'commerce_product_type_options_list',
        ),
      ),
      'module' => 'commerce_discount',
      'group' => t('Commerce Discount'),
      'callbacks' => array(
        'execute' => $inline_conditions['commerce_product_has_type']['callbacks']['build'],
      ),
    );
  }
  if (module_exists('taxonomy') && module_exists('commerce_product')) {
    $conditions['commerce_product_has_specified_terms'] = array(
      'label' => t('Line item product contains specific terms ID'),
      'parameter' => array(
        'commerce_line_item' => array(
          'type' => 'commerce_line_item',
          'label' => t('Line item'),
          'description' => t('The line item.'),
          'wrapped' => TRUE,
        ),
        'terms' => array(
          'type' => 'text',
          'label' => t('Terms ID'),
          'description' => t('Enter a comma-separated list of term ID to compare against the passed product line item.'),
        ),
      ),
      'module' => 'commerce_discount',
      'group' => t('Commerce Discount'),
      'callbacks' => array(
        'execute' => $inline_conditions['commerce_product_has_specified_terms']['callbacks']['build'],
      ),
    );
  }

  // Add a very simple boolean condition to check whether or not a line item was
  // added to an order via a discount. This should likely be accompanied by an
  // action to load that discount and additional conditions to check the name,
  // type, and offer type of the discount.
  $conditions['commerce_discount_line_item_added_by_discount'] = array(
    'label' => t('Line item added by discount'),
    'parameter' => array(
      'commerce_line_item' => array(
        'type' => 'commerce_line_item',
        'label' => t('Line item'),
      ),
    ),
    'group' => t('Commerce Discount'),
  );
  return $conditions;
}

/**
 * Implements hook_rules_action_info().
 */
function commerce_discount_rules_action_info() {
  $types = commerce_discount_offer_types();
  $items = array();
  foreach ($types as $info) {
    $items[$info['action']] = array(
      'label' => $info['label'],
      'parameter' => array(
        'entity' => array(
          'label' => t('Entity'),
          'type' => 'entity',
          'wrapped' => TRUE,
        ),
        'commerce_discount' => array(
          'label' => t('Commerce Discount'),
          'type' => 'token',
          'options list' => 'commerce_discount_entity_list',
        ),
      ),
      'group' => t('Commerce Discount'),
      'base' => $info['action'],
    );
  }
  $items['commerce_discount_remove_discount_components_on_products'] = array(
    'label' => t('Remove discount price components'),
    'group' => t('Commerce Discount'),
    'parameter' => array(
      'commerce_line_item' => array(
        'type' => 'commerce_line_item',
        'label' => t('Line item'),
        'wrapped' => TRUE,
      ),
    ),
  );
  $items['commerce_discount_shipping_service'] = array(
    'label' => t('Apply shipping discount'),
    'group' => t('Commerce Discount'),
    'parameter' => array(
      'entity' => array(
        'label' => t('Entity'),
        'type' => 'entity',
        'wrapped' => TRUE,
      ),
      'commerce_discount' => array(
        'label' => t('Commerce Discount'),
        'type' => 'token',
        'options list' => 'commerce_discount_entity_list',
      ),
    ),
    'base' => 'commerce_discount_shipping_service',
  );
  $items['commerce_discount_shipping_services'] = array(
    'label' => t('Discount shipping services'),
    'parameter' => array(
      'commerce_order' => array(
        'type' => 'commerce_order',
        'label' => t('Order'),
      ),
    ),
    'group' => t('Commerce Discount'),
  );
  return $items;
}

/**
 * Implements hook_rules_event_info().
 */
function commerce_discount_rules_event_info() {
  $items = array();
  $items['commerce_discount_order'] = array(
    'label' => t('Apply a discount to a given order'),
    'group' => t('Commerce Discount'),
    'variables' => entity_rules_events_variables('commerce_order', t('Order', array(), array(
      'context' => 'a drupal commerce order',
    ))),
    'access callback' => 'commerce_order_rules_access',
  );
  return $items;
}

/**
 * Rules callback: executes the "Check discount compatibility at the order
 * level" condition.
 *
 * @param EntityDrupalWrapper $order_wrapper
 *   The order the discount would be applied to.
 * @param string $discount_name
 *   The discount name whose compatibility needs to be checked.
 *
 * @return bool
 *   Returns TRUE if nothing has disqualified compatibility.
 */
function commerce_discount_compatibility_check(EntityDrupalWrapper $order_wrapper, $discount_name) {

  // Ensure the discount we're loading still exists.
  if (!($discount = entity_load_single('commerce_discount', $discount_name))) {
    return FALSE;
  }

  // Use the order total price field for order level compatibility checks.
  $order_total = $order_wrapper->commerce_order_total
    ->value();
  $return = _commerce_discount_check_compatibility($discount, $order_total);
  return $return;
}

/**
 * Rules callback: executes the "Check discount compatibility at the line item
 * level" condition.
 *
 * @param EntityDrupalWrapper $line_item_wrapper
 *   The line item the discount would be applied to.
 * @param string $discount_name
 *   The discount name whose compatibility needs to be checked.
 *
 * @return bool
 *   Returns TRUE if nothing has disqualified compatibility.
 */
function commerce_discount_line_item_compatibility_check(EntityDrupalWrapper $line_item_wrapper, $discount_name) {

  // Ensure the discount we're loading still exists.
  if (!($discount = entity_load_single('commerce_discount', $discount_name))) {
    return FALSE;
  }

  // Use the line item unit price field for line item level compatibility checks.
  $unit_price = $line_item_wrapper->commerce_unit_price
    ->value();
  return _commerce_discount_check_compatibility($discount, $unit_price);
}

/**
 * Performs a discount compatibility check for the given price field.
 *
 * @param object $discount
 *   The discount entity whose compatibility is being checked.
 * @param array $price
 *   The price field value whose components array indicates which discounts have
 *   already been applied to it.
 *
 * @return bool
 *   Boolean indicating whether or not the compatibility check passed.
 */
function _commerce_discount_check_compatibility($discount, $price) {

  // Get the strategy from the compatibility strategy field.
  $strategy = commerce_discount_get_compatibility_strategy($discount);

  // Determine which discounts of a similar discount type have been applied to
  // the price thus far. Compatibility checks currently only take into account
  // discounts of the same type due to process limitations (i.e. Product
  // discounts *always* apply before Order discounts) and usability constraints
  // (i.e. we have data to make Order discount applicability dependent on
  // Product discounts, but we do not have a clear way to indicate that uni-
  // directionality in the UI yet).
  $applied_discounts = commerce_discount_get_discounts_applied_to_price($price, $discount->type);

  // Ensure none of them indicate they are incompatible with the current one.
  foreach ($applied_discounts as $applied_discount_id => $applied_discount_name) {

    // Determine the strategy of the existing discount.
    $existing_discount = entity_load_single('commerce_discount', $applied_discount_name);
    $existing_strategy = commerce_discount_get_compatibility_strategy($existing_discount);

    // If the discount is compatible with any other discount, continue on.
    if ($existing_strategy == 'any') {
      continue;
    }

    // If the discount is not compatible with any other discount, return FALSE.
    if ($existing_strategy == 'none') {
      return FALSE;
    }

    // Get the selected discount value from the existing discount.
    $existing_selection = commerce_discount_get_compatibility_selection($existing_discount);

    // If the discount is incompatible with select discounts and the current
    // discount was selected, return FALSE.
    if ($existing_strategy == 'except' && in_array($discount->discount_id, $existing_selection)) {
      return FALSE;
    }

    // If the discount is only compatible with select discounts and the current
    // discount was not selected, return FALSE.
    if ($existing_strategy == 'only' && !in_array($discount->discount_id, $existing_selection)) {
      return FALSE;
    }
  }

  // If the discount is not compatible with any other discount and we found
  // applied discounts, return FALSE.
  if ($strategy == 'none' && count($applied_discounts) > 0) {
    return FALSE;
  }

  // Get the selected discount value from the discount being evaluated.
  $selection = commerce_discount_get_compatibility_selection($discount);

  // If the discount is not compatible with selected discounts, ensure they
  // are not on the order.
  if ($strategy == 'except') {
    foreach ($applied_discounts as $applied_discount_id => $applied_discount_name) {
      if (in_array($applied_discount_id, $selection)) {
        return FALSE;
      }
    }
  }

  // If the discount is only compatible with selected discounts, ensure no other
  // discounts are on the order.
  if ($strategy == 'only') {
    foreach ($applied_discounts as $applied_discount_id => $applied_discount_name) {
      if (!in_array($applied_discount_id, $selection)) {
        return FALSE;
      }
    }
  }

  // Return TRUE if nothing has disqualified compatibility yet. For example, if
  // the discount is compatible with any other discount and no other discounts
  // on the order specifies an incompatibility with this discount or if this
  // discount were incompatible with other discounts but none have been applied
  // to the order yet.
  return TRUE;
}

/**
 * Options list for commerce_order_compare_order_amount line item types.
 */
function commerce_order_compare_order_amount_options_list() {
  return array_diff_key(commerce_line_item_type_options_list(), drupal_map_assoc(array(
    'commerce_discount',
    'product_discount',
  )));
}

/**
 * Default value array for commerce_order_compare_order_amount line item types.
 */
function commerce_order_compare_order_amount_options_default() {
  return drupal_map_assoc(array_keys(commerce_order_compare_order_amount_options_list()));
}

/**
 * Build callback for commerce_order_compare_order_amount.
 *
 * @param EntityDrupalWrapper $wrapper
 *   The wrapped entity given by the rule.
 * @param string $operator
 *   The comparison operator.
 * @param array $total
 *   A commerce_price type array.
 *
 * @return bool
 *   return true if condition is valid. false otherwise.
 */
function commerce_order_compare_order_amount_build(EntityDrupalWrapper $wrapper, $operator, $total, $line_item_types) {
  $total_order = 0;

  // Ensure the discount currency code is the same as the order.
  if ($wrapper->commerce_order_total->currency_code
    ->value() != $total['currency_code']) {
    return FALSE;
  }

  // If $line_item_types is not an array, then we need to total all line item
  // types to preserve backwards compatibility.
  if (!is_array($line_item_types)) {
    $line_item_types = commerce_order_compare_order_amount_options_default();
  }

  // Get given total order amount.
  foreach ($wrapper->commerce_line_items as $line_item_wrapper) {
    if (in_array($line_item_wrapper
      ->getBundle(), $line_item_types, TRUE)) {

      // Convert the line item's total to the order's currency for totalling.
      $component_total = commerce_price_component_total($line_item_wrapper->commerce_total
        ->value());

      // Add the totals.
      $total_order += commerce_currency_convert($component_total['amount'], $component_total['currency_code'], $total['currency_code']);
    }
  }
  switch ($operator) {
    case '<':
      return $total_order < $total['amount'];
    case '<=':
      return $total_order <= $total['amount'];
    case '==':
      return $total_order == $total['amount'];
    case '>':
      return $total_order > $total['amount'];
    case '>=':
      return $total_order >= $total['amount'];
    default:
      return FALSE;
  }
}

/**
 * Build callback for commerce_order_has_owner.
 *
 * @param EntityDrupalWrapper $wrapper
 *   The wrapped entity given by the rule.
 * @param array $account
 *   A fully loaded drupal user.
 *
 * @return bool
 *   Returns true if condition is valid. false otherwise.
 */
function commerce_order_has_owner_build(EntityDrupalWrapper $wrapper, $account) {
  if (isset($account->uid)) {

    // If current logged user matches the discount related users.
    return $account->uid == $wrapper->uid
      ->value();
  }
  return FALSE;
}

/**
 * Build callback for commerce_order_contains_products.
 *
 * @param EntityDrupalWrapper $wrapper
 *   The wrapped entity given by the rule.
 * @param string $products
 *   A list of products SKU.
 * @param string $operator
 *   An optional operator for comparison. The default is 'any'.
 *
 * @return bool
 *   Returns True if condition is valid. False otherwise.
 */
function commerce_order_contains_products_build(EntityDrupalWrapper $wrapper, $products, $operator = 'any') {

  // Split by comma. Allow some grace for spaces.
  $products_sku = explode(', ', (string) $products);
  $order_skus = array();
  foreach ($wrapper->commerce_line_items as $wrapper_line_item) {

    // Ensures the passed line item is a product.
    if (in_array('commerce_product', array_keys($wrapper_line_item
      ->getPropertyInfo()))) {
      $order_skus[] = $wrapper_line_item->commerce_product->sku
        ->value();
    }
  }

  // Products that are in the list but not on the order.
  $list_but_not_order = array_diff($products_sku, $order_skus);
  if ($operator == 'all') {

    // If the array is empty, then everything in the list is on the order.
    return count($list_but_not_order) == 0;
  }
  elseif ($operator == 'any') {

    // If the number of items in the list isn't the same as the products_sku,
    // then we know that at least one of the items is on the order.
    return count($list_but_not_order) != count($products_sku);
  }
  else {

    // Generate a list of products that are on the order but not on the list.
    $order_but_not_list = array_diff($order_skus, $products_sku);
    if ($operator == 'only') {

      // If all of the order SKUs are on the list and the list of product SKUs
      // is not the same as the original list, then the order contains at least
      // one of the products in the list and doesn't include any products not
      // on the list.
      return empty($order_but_not_list) && count($list_but_not_order) != count($products_sku);
    }
    elseif ($operator == 'exactly') {

      // If both arrays are empty, then the order contains every product on
      // the list but no additional products.
      return empty($list_but_not_order) && empty($order_but_not_list);
    }
  }
  return FALSE;
}

/**
 * Build callback for commerce_product_has_type.
 *
 * @param EntityDrupalWrapper $wrapper
 *   Wrapped entity type given by the rule.
 * @param string $type
 *   Product type returned by rule condition.
 *
 * @return bool
 *   True if condition is valid. false otherwise.
 */
function commerce_product_has_type_build(EntityDrupalWrapper $wrapper, $type) {

  // Only for Line items with Product reference field.
  if (in_array('commerce_product', array_keys($wrapper
    ->getPropertyInfo()))) {
    return $wrapper->commerce_product->type
      ->value() == $type;
  }
  return FALSE;
}

/**
 * Build callback for inline_conditions_product_quantity.
 *
 * Checks if every order line item match the quantity comparison defined in the
 * rule settings.
 *
 * @param EntityDrupalWrapper $wrapper
 *   Wrapped entity given by the rule.
 * @param string $products
 *   A list of products SKU.
 * @param string $operator
 *   String operator used to compare product quantity.
 * @param int $quantity
 *   Product quantity.
 *
 * @return bool
 *   True if the condition is valid. False otherwise.
 */
function commerce_order_has_specific_quantity_products_build(EntityDrupalWrapper $wrapper, $products, $operator, $quantity) {
  $products_sku = explode(', ', (string) $products);

  // Loop on order line items to check if each product has the quantity
  // specified in the rule settings.
  foreach ($wrapper->commerce_line_items as $wrapper_line_item) {
    if (in_array('commerce_product', array_keys($wrapper_line_item
      ->getPropertyInfo()))) {
      if (($key = array_search($wrapper_line_item->commerce_product->sku
        ->value(), $products_sku)) !== FALSE) {

        // At this point, we are sure that the current product is in the order.
        // If this product line item doesn't meet the quantity comparison, the
        // condition will return false.
        switch ($operator) {
          case '<':
            if ($wrapper_line_item->quantity
              ->value() < $quantity) {
              unset($products_sku[$key]);
            }
            else {
              return FALSE;
            }
            break;
          case '<=':
            if ($wrapper_line_item->quantity
              ->value() <= $quantity) {
              unset($products_sku[$key]);
            }
            else {
              return FALSE;
            }
            break;
          case '==':
            if ($wrapper_line_item->quantity
              ->value() == $quantity) {
              unset($products_sku[$key]);
            }
            else {
              return FALSE;
            }
            break;
          case '>':
            if ($wrapper_line_item->quantity
              ->value() > $quantity) {
              unset($products_sku[$key]);
            }
            else {
              return FALSE;
            }
            break;
          case '>=':
            if ($wrapper_line_item->quantity
              ->value() >= $quantity) {
              unset($products_sku[$key]);
            }
            else {
              return FALSE;
            }
            break;
        }
      }
    }
  }
  return empty($products_sku);
}

/**
 * Build callback for commerce_product_contains_products.
 *
 * @param EntityDrupalWrapper $wrapper
 *   Wrapped entity type given by the rule.
 * @param string $sku
 *   Product sku(s) returned by rule condition.
 *
 * @return bool
 *   True if condition is valid. false otherwise.
 */
function commerce_product_contains_products_build(EntityDrupalWrapper $wrapper, $sku) {

  // Only for Line items with Product reference field.
  if (in_array('commerce_product', array_keys($wrapper
    ->getPropertyInfo()))) {
    return in_array($wrapper->commerce_product->sku
      ->value(), array_map('trim', explode(',', $sku)));
  }
  return FALSE;
}

/**
 * Build callback for commerce_product_has_specified_terms on product.
 *
 * @param EntityDrupalWrapper $wrapper
 *   Wrapped entity type given by the rule.
 * @param array $terms
 *   Values for the condition settings.
 *
 * @return bool
 *   True is condition is valid. false otherwise.
 */
function commerce_product_has_specified_terms_build(EntityDrupalWrapper $wrapper, $terms) {
  $terms_name = explode(', ', $terms);
  if (in_array('commerce_product', array_keys($wrapper
    ->getPropertyInfo()))) {

    // Fetch all the fields name of taxonomy_term type for the passed entity.
    foreach ($wrapper->commerce_product
      ->getPropertyInfo() as $field_name => $property) {
      if (preg_match('/taxonomy_term/', $property['type'])) {

        // If the wrapped field is an instance of EntityListWrapper class, that
        // means that field contains multiple values.
        if ($wrapper->commerce_product->{$field_name} instanceof EntityListWrapper) {
          foreach ($wrapper->commerce_product->{$field_name} as $wrapper_term) {
            if (($key = array_search($wrapper_term
              ->getIdentifier(), $terms_name)) !== FALSE) {
              unset($terms_name[$key]);
            }
          }
        }
        elseif ($term = $wrapper->commerce_product->{$field_name}
          ->value()) {
          if (($key = array_search($term->tid, $terms_name)) !== FALSE) {
            unset($terms_name[$key]);
          }
        }
      }
    }
  }
  return empty($terms_name);
}

/**
 * Condition callback: looks to see if the line item was added by a discount.
 */
function commerce_discount_line_item_added_by_discount($line_item) {
  return !empty($line_item->data['discount_name']);
}

/**
 * Rules action: Apply fixed amount discount.
 */
function commerce_discount_fixed_amount(EntityDrupalWrapper $wrapper, $discount_name) {
  $discount_wrapper = entity_metadata_wrapper('commerce_discount', $discount_name);
  $discount_price = $discount_wrapper->commerce_discount_offer->commerce_fixed_amount
    ->value();
  $discount_price['amount'] = -$discount_price['amount'];

  // Get the line item types to apply the discount to.
  $line_item_types = variable_get('commerce_discount_line_item_types', array_diff(commerce_product_line_item_types(), array(
    'product_discount',
  )));
  switch ($wrapper
    ->type()) {
    case 'commerce_order':

      // Exit if the wrapper doesn't have a commerce_discounts property.
      if (!isset($wrapper->commerce_discounts)) {
        return;
      }

      // Set reference to the discount.
      // @todo: It doesn't work with the wrapper.
      $order = $wrapper
        ->value();

      // If the discount will bring the order to less than zero, set the
      // discount amount so that it stops at zero.
      $order_amount = $wrapper->commerce_order_total->amount
        ->value();
      if (-$discount_price['amount'] > $order_amount) {
        $discount_price['amount'] = -$order_amount;
      }
      $order->commerce_discounts[LANGUAGE_NONE][]['target_id'] = $discount_wrapper->discount_id
        ->value();

      // Modify the existing discount line item or add a new one if that fails.
      if (!commerce_discount_set_existing_line_item_price($wrapper, $discount_name, $discount_price)) {
        commerce_discount_add_line_item($wrapper, $discount_name, $discount_price);
      }

      // Update the total order price, for the next rules condition (if any).
      commerce_discount_calculate_order_total($wrapper);
      break;
    case 'commerce_line_item':

      // Check if the line item is configured in the settings to apply the
      // discount.
      if (!in_array($wrapper
        ->getBundle(), $line_item_types)) {
        return;
      }

      // Check whether this discount was already added as a price component.
      $unit_price = commerce_price_wrapper_value($wrapper, 'commerce_unit_price', TRUE);
      foreach ($unit_price['data']['components'] as $component) {
        if (!empty($component['price']['data']['discount_name']) && $component['price']['data']['discount_name'] == $discount_wrapper
          ->getIdentifier()) {
          return;
        }
      }

      // Do not allow negative line item totals.
      $line_item_amount = $wrapper->commerce_unit_price->amount
        ->value();
      if (-$discount_price['amount'] > $line_item_amount) {
        $discount_price['amount'] = -$line_item_amount;
      }
      commerce_discount_add_price_component($wrapper, $discount_name, $discount_price);
      break;
  }
}

/**
 * Rules action: Apply percentage discount.
 */
function commerce_discount_percentage(EntityDrupalWrapper $wrapper, $discount_name) {
  $discount_wrapper = entity_metadata_wrapper('commerce_discount', $discount_name);
  $rate = $discount_wrapper->commerce_discount_offer->commerce_percentage
    ->value() / 100;

  // Get the line item types to apply the discount to.
  $line_item_types = variable_get('commerce_discount_line_item_types', array_diff(commerce_product_line_item_types(), array(
    'product_discount',
  )));

  // Filter out 0 values in the variable.
  $line_item_types = array_filter($line_item_types);
  switch ($wrapper
    ->type()) {
    case 'commerce_order':

      // Exit if there are no line items or the wrapper doesn't contain
      // the commerce_discounts property.
      if (!isset($wrapper->commerce_discounts) || !$wrapper->commerce_line_items
        ->value()) {
        return;
      }

      // Set reference to the discount.
      // @todo: It doesn't work with the wrapper.
      $order = $wrapper
        ->value();
      $order->commerce_discounts[LANGUAGE_NONE][]['target_id'] = $discount_wrapper->discount_id
        ->value();
      $calculated_discount = 0;

      // Loop the line items of the order and calculate the total discount.
      foreach ($wrapper->commerce_line_items as $line_item_wrapper) {

        // Check if the line item is configured in the discount settings to
        // apply the discount.
        $line_item_type = $line_item_wrapper
          ->getBundle();
        if (in_array($line_item_type, $line_item_types, TRUE)) {
          $line_item_total = commerce_price_wrapper_value($line_item_wrapper, 'commerce_total', TRUE);
          $calculated_discount += $line_item_total['amount'] * $rate;
        }
      }
      if ($calculated_discount) {
        $discount_amount = array(
          'amount' => $calculated_discount * -1,
          'currency_code' => $wrapper->commerce_order_total->currency_code
            ->value(),
        );

        // Modify the existing discount line item or add a new line item
        // if that fails.
        if (!commerce_discount_set_existing_line_item_price($wrapper, $discount_name, $discount_amount)) {
          commerce_discount_add_line_item($wrapper, $discount_name, $discount_amount);
        }

        // Update the total order price, for the next rules condition (if any).
        commerce_discount_calculate_order_total($wrapper);
      }
      break;
    case 'commerce_line_item':

      // Check if the line item is configured in the discount settings to apply
      // the discount.
      $line_item_type = $wrapper
        ->getBundle();
      if (!in_array($line_item_type, $line_item_types, TRUE)) {
        return;
      }

      // Check whether this discount was already added as a price component.
      $unit_price = commerce_price_wrapper_value($wrapper, 'commerce_unit_price', TRUE);
      foreach ($unit_price['data']['components'] as $component) {
        if (!empty($component['price']['data']['discount_name']) && $component['price']['data']['discount_name'] == $discount_name) {
          return;
        }
      }
      $discount_amount = array(
        'amount' => $unit_price['amount'] * $rate * -1,
        'currency_code' => $unit_price['currency_code'],
      );
      commerce_discount_add_price_component($wrapper, $discount_name, $discount_amount);
      break;
  }
}

/**
 * Rules action: Apply shipping discount.
 *
 * @param EntityDrupalWrapper $order_wrapper
 *   The wrapped order entity.
 * @param string $discount_name
 *   The name of the discount.
 */
function commerce_discount_shipping_service($order_wrapper, $discount_name) {
  $order = $order_wrapper
    ->value();

  // Shipping discount rules actions run on two occasions:
  // 1. When selecting the shipping service on checkout. This time
  //   $order->shipping_rates is already set. The cart refresh is triggered
  //   manually in commerce_discount_commerce_shipping_method_collect_rates()
  //   and we don't save the shipping line items, since we create them only to
  //   display the price.
  // 2. On any other cart refresh the $order->shipping_rates is initially
  //   empty so we set it to the shipping line item present on the order (if
  //   any). In this case we want to save the shipping line item.
  // What is common in the two cases is that we calculate the discount only
  // for the shipping services in $order->shipping_rates.
  // If the order hasn't had any shipping rates calculated for it, check if
  // there's already a shipping line item referenced by the order.
  if (empty($order->shipping_rates)) {
    foreach ($order_wrapper->commerce_line_items as $line_item_wrapper) {
      if ($line_item_wrapper
        ->value() && $line_item_wrapper
        ->getBundle() === 'shipping') {
        $shipping_service = $line_item_wrapper->commerce_shipping_service
          ->value();
        $order->shipping_rates[$shipping_service] = $line_item_wrapper
          ->value();
      }
    }
    if (empty($order->shipping_rates)) {
      return;
    }
  }

  // Load the discount to find the free shipping service and strategy.
  $discount = entity_load_single('commerce_discount', $discount_name);
  $discount_wrapper = entity_metadata_wrapper('commerce_discount', $discount);
  $discount_offer_wrapper = $discount_wrapper->commerce_discount_offer;
  $shipping_discount_offer_type = $discount_offer_wrapper
    ->getBundle();
  switch ($shipping_discount_offer_type) {
    case 'percent_off_shipping':
      $shipping_service_to_discount = $discount_offer_wrapper->commerce_percent_off_ship_serv
        ->value();

      // Determine the discount multiplier based on submitted value.
      $discount_value = $discount_offer_wrapper->commerce_percent_off_shipping
        ->value();
      if ($discount_value > 1 && $discount_value <= 100) {
        $discount_multiplier = $discount_value / 100;
      }
      else {
        return;
      }
      foreach ($order->shipping_rates as $shipping_service => $shipping_object) {

        // Check if the order contains the shipping rate we want to apply a
        // discount against. Not all shipping rates apply to all orders.
        if (isset($shipping_service_to_discount) && $shipping_service_to_discount !== $shipping_service) {
          continue;
        }

        // Instantiate the line item wrapper.
        $shipping_line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $order->shipping_rates[$shipping_service]);

        // Calculate the correct value to discount the line item.
        $discount_shipping_value = -$shipping_line_item_wrapper->commerce_unit_price->amount
          ->value() * $discount_multiplier;
        _commerce_discount_add_shipping_discount_price_component($shipping_line_item_wrapper, $order_wrapper, $discount_name, $discount_shipping_value);
      }
      break;
    case 'free_shipping':
      try {
        $free_service = $discount_wrapper->commerce_discount_offer->commerce_free_shipping
          ->raw();
        $free_shipping_strategy = commerce_discount_get_free_shipping_strategy($discount);
      } catch (Exception $e) {
        watchdog('commerce_discount', 'Free shipping configuration issue detected on discount %name.', array(
          '%name' => $discount_wrapper
            ->label(),
        ));
        return;
      }

      // If a shipping service is specified, ensure it exists.
      if (!empty($free_service)) {
        if (!empty($order->shipping_rates[$free_service])) {

          // Exit if the shipping service is already free.
          $free_service_wrapper = entity_metadata_wrapper('commerce_line_item', $order->shipping_rates[$free_service]);
          if ($free_service_wrapper->commerce_unit_price->amount
            ->value() <= 0) {
            return;
          }

          // Update the free shipping service line item's rate to be free using the
          // service's price.
          $discount_amount = $free_service_wrapper->commerce_unit_price->amount
            ->value();
          $order->shipping_rates[$free_service]->data['shipping_service']['description'] .= ' <span class="shipping-discount-text">' . check_plain($discount->component_title) . '</span>';
          _commerce_discount_add_shipping_discount_price_component($free_service_wrapper, $order_wrapper, $discount_name, -$discount_amount);
        }

        // The free shipping discount strategy requires us to discount all other
        // shipping services an equivalent amount.
        if ($free_shipping_strategy == 'discount_all') {
          foreach ($order->shipping_rates as $service_name => $line_item) {

            // Skip the free shipping service.
            if ($service_name == $free_service) {
              continue;
            }
            $free_shipping_service = commerce_shipping_service_load($free_service);
            $callback = commerce_shipping_service_callback($free_shipping_service, 'rate');
            $price = $callback($free_shipping_service, $order);
            $discount_amount = $price['amount'];

            // Apply the discount, ensuring the rate does not discount below 0,
            // but do not use the message again to reduce spam on the form.
            $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
            $service_discount_amount = min($discount_amount, $line_item_wrapper->commerce_unit_price->amount
              ->value());
            _commerce_discount_add_shipping_discount_price_component($line_item_wrapper, $order_wrapper, $discount_name, -$service_discount_amount);
          }
        }
      }
      break;
    case 'shipping_upgrade':
      try {
        $target_service_id = $discount_offer_wrapper->commerce_shipping_upgrade_target
          ->raw();
        $source_service_id = $discount_offer_wrapper->commerce_shipping_upgrade_source
          ->raw();
      } catch (Exception $e) {
        watchdog('commerce_discount', 'Shipping service upgrade configuration issue detected on discount %name.', array(
          '%name' => $discount_wrapper
            ->label(),
        ));
        return;
      }

      // Exit now if we either could not determine a target or source service or
      // they were not calculated for this order.
      if (empty($target_service_id) || empty($source_service_id) || empty($order->shipping_rates[$target_service_id])) {
        return;
      }
      $target_line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $order->shipping_rates[$target_service_id]);

      // To calculate the source shipping rate with discounts, we need to
      // remove the target service from the order and apply the source instead.
      $cloned_order = clone $order;
      $cloned_order_wrapper = entity_metadata_wrapper('commerce_order', $cloned_order);
      if (empty($cloned_order->shipping_rates[$source_service_id])) {
        foreach ($cloned_order_wrapper->commerce_line_items as $delta => $line_item_wrapper) {

          // If this line item is a shipping line item...
          if ($line_item_wrapper
            ->value() && $line_item_wrapper
            ->getBundle() === 'shipping') {
            $original_shipping_line_item = $line_item_wrapper
              ->value();
            $cloned_order_wrapper->commerce_line_items
              ->offsetUnset($delta);
            unset($cloned_order->shipping_rates[$target_service_id]);
          }
        }
        module_load_include('inc', 'commerce_shipping', 'commerce_shipping.rules');
        commerce_shipping_rate_apply($cloned_order, $source_service_id);

        // Get the new line item.
        $line_items = field_get_items('commerce_order', $cloned_order, 'commerce_line_items');
        $line_item_id = array_pop($line_items);
        $source_shipping_line_item = commerce_line_item_load($line_item_id['line_item_id']);
        $cloned_order->shipping_rates[$source_service_id] = $source_shipping_line_item;

        // Apply discounts on source line item. This won't create an endless
        // loop since we have unset the source service on the order above.
        commerce_discount_commerce_cart_order_refresh($cloned_order_wrapper);
      }

      // Get the discounted rate for the source service.
      $source_line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $cloned_order->shipping_rates[$source_service_id]);

      // If the prices are already the same, exit now.
      if ($target_line_item_wrapper->commerce_unit_price->amount
        ->value() <= $source_line_item_wrapper->commerce_unit_price->amount
        ->value()) {
        return;
      }

      // Otherwise, calculate the difference between the source and the target.
      $difference = $source_line_item_wrapper->commerce_unit_price->amount
        ->value() - $target_line_item_wrapper->commerce_unit_price->amount
        ->value();
      _commerce_discount_add_shipping_discount_price_component($target_line_item_wrapper, $order_wrapper, $discount_name, $difference);
      break;
  }
}

/**
 * Helper function for the shipping discount rules action.
 *
 * @param EntityDrupalWrapper $line_item_wrapper
 *   The wrapped line item object to add the discount price component to.
 * @param EntityDrupalWrapper $order_wrapper
 *   The wrapped order entity.
 * @param string $discount_name
 *   The discount machine name.
 * @param integer $amount
 *   The (negative) discount amount to add as price component.
 *
 * @return bool
 *   A boolean indicating whether or not the discount price component has
 *   been added.
 */
function _commerce_discount_add_shipping_discount_price_component($line_item_wrapper, $order_wrapper, $discount_name, $amount) {

  // Prevent the order total from going negative.
  $order_total = $order_wrapper->commerce_order_total
    ->value();
  if (-$amount > $order_total['amount']) {
    $amount = -$order_total['amount'];
  }
  if ($amount >= 0) {
    return FALSE;
  }
  $discount_price = array(
    'amount' => $amount,
    'currency_code' => $line_item_wrapper->commerce_unit_price->currency_code
      ->value(),
  );
  commerce_discount_add_price_component($line_item_wrapper, $discount_name, $discount_price);
  if ($line_item_wrapper
    ->getIdentifier() !== FALSE) {

    // Save the line item so that it correctly appears in the subsequent
    // order total calculation.
    $line_item_wrapper
      ->save();
    commerce_discount_calculate_order_total($order_wrapper);
    return TRUE;
  }
  return FALSE;
}

/**
 * Rules action: Apply free bonus products discount.
 *
 * @param EntityDrupalWrapper $wrapper
 *   Wrapped commerce_order entity type.
 * @param string $discount_name
 *   The name of the discount.
 */
function commerce_discount_free_products(EntityDrupalWrapper $wrapper, $discount_name) {
  $discount_wrapper = entity_metadata_wrapper('commerce_discount', $discount_name);
  if (!($products = $discount_wrapper->commerce_discount_offer->commerce_free_products
    ->value())) {
    return;
  }

  // Exit if the wrapper doesn't have a commerce_discounts property.
  if (!isset($wrapper->commerce_discounts)) {
    return;
  }

  // Set reference to the discount.
  $order = $wrapper
    ->value();
  $order->commerce_discounts[LANGUAGE_NONE][]['target_id'] = $discount_wrapper->discount_id
    ->value();

  // Loop on products and add each product to order line items.
  foreach ($products as $product) {
    $context = array(
      'commerce_discount_offer' => 'free_products',
      'discount_name' => $discount_name,
    );

    // Look for a product display that references this project. This is
    // imprecise but covers the vastly most common use case.
    $node_instances = field_info_instances('node');
    foreach ($node_instances as $bundle => $instances) {
      foreach ($instances as $instance) {
        $field = field_info_field($instance['field_name']);
        if ($field['type'] == 'commerce_product_reference') {
          $query = new EntityFieldQuery();
          $results = $query
            ->entityCondition('entity_type', 'node')
            ->fieldCondition($instance['field_name'], 'product_id', $product->product_id)
            ->propertyCondition('status', 1)
            ->execute();
          if (!empty($results['node'])) {
            $nids = array_keys($results['node']);

            // Just take the first one.
            $node = node_load(reset($nids));
            $uri = entity_uri('node', $node);

            // Build the context array in order to attach the display path on
            // product level.
            $context += array(
              'entity_id' => $node->nid,
              'entity_type' => 'node',
              'display_path' => $uri['path'],
            );

            // If we have found something, no need to continue.
            break;
          }
        }
      }
    }

    // This covers use cases where the above logic is insufficient.
    $discount = $discount_wrapper
      ->value();
    drupal_alter('commerce_discount_free_product_context', $context, $product, $discount);
    $data = array(
      'context' => $context,
    );
    $line_item = commerce_product_line_item_new($product, 1, $order->order_id, $data, 'product_discount');
    $wrapper_line_item = entity_metadata_wrapper('commerce_line_item', $line_item);

    // Getting the product price and negate it for the discount component price.
    $product_unit_price = commerce_price_wrapper_value(entity_metadata_wrapper('commerce_product', $product), 'commerce_price');
    $discount_amount = array(
      'amount' => -$product_unit_price['amount'],
      'currency_code' => $product_unit_price['currency_code'],
    );
    commerce_discount_add_price_component($wrapper_line_item, $discount_name, $discount_amount);
    commerce_line_item_save($line_item);

    // Add the free bonus product to order's line items.
    $wrapper->commerce_line_items[] = $line_item;
    commerce_discount_calculate_order_total($wrapper);
  }
}

/**
 * Creates a discount line item on the provided order.
 *
 * @param EntityDrupalWrapper $order_wrapper
 *   The wrapped order entity.
 * @param string $discount_name
 *   The name of the discount being applied.
 * @param array $discount_amount
 *   The discount amount price array (amount, currency_code).
 * @param array $data
 *   Optional. An associative array of parameters to include in the line item's
 *   data array along with the discount name.
 *
 * @return object
 *   The newly created discount line item.
 */
function commerce_discount_add_line_item(EntityDrupalWrapper $order_wrapper, $discount_name, $discount_amount, $data = array()) {

  // Create a new line item.
  $discount_line_item = commerce_line_item_new('commerce_discount', $order_wrapper
    ->getIdentifier());
  $discount_line_item->data = array(
    'discount_name' => $discount_name,
  ) + $data;

  // Setting the bundle to ensure the entity metadata stores it correctly.
  $info = array(
    'bundle' => 'commerce_discount',
  );
  $discount_line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $discount_line_item, $info);

  // Initialize the line item unit price.
  $discount_line_item_wrapper->commerce_unit_price->amount = 0;
  $discount_line_item_wrapper->commerce_unit_price->currency_code = $discount_amount['currency_code'];

  // Reset the data array of the line item total field to only include a base
  // price component and set the currency code from the order.
  $base_price = array(
    'amount' => 0,
    'currency_code' => $discount_amount['currency_code'],
    'data' => array(),
  );
  $discount_line_item_wrapper->commerce_unit_price->data = commerce_price_component_add($base_price, 'base_price', $base_price, TRUE);

  // Add the discount price component.
  commerce_discount_add_price_component($discount_line_item_wrapper, $discount_name, $discount_amount);

  // Save the incoming line item now so we get its ID and add it to the oder's
  // line item reference field value.
  commerce_line_item_save($discount_line_item);
  $order_wrapper->commerce_line_items[] = $discount_line_item;
  return $discount_line_item;
}

/**
 * Adds a discount price component to the provided line item.
 *
 * @param EntityDrupalWrapper $line_item_wrapper
 *   The wrapped line item entity.
 * @param string $discount_name
 *   The name of the discount being applied.
 * @param array $discount_amount
 *   The discount amount price array (amount, currency_code).
 */
function commerce_discount_add_price_component(EntityDrupalWrapper $line_item_wrapper, $discount_name, $discount_amount) {
  $unit_price = commerce_price_wrapper_value($line_item_wrapper, 'commerce_unit_price', TRUE);
  $discount_wrapper = entity_metadata_wrapper('commerce_discount', $discount_name);
  $component_title = $discount_wrapper->component_title
    ->value();
  $current_amount = $unit_price['amount'];

  // Currencies don't match, abort.
  if ($discount_amount['currency_code'] != $unit_price['currency_code']) {
    return;
  }

  // Calculate the updated amount and create a price array representing the
  // difference between it and the current amount.
  $discount_amount['amount'] = commerce_round(COMMERCE_ROUND_HALF_UP, $discount_amount['amount']);
  $updated_amount = $current_amount + $discount_amount['amount'];
  $difference = array(
    'amount' => $discount_amount['amount'],
    'currency_code' => $discount_amount['currency_code'],
    'data' => array(
      'discount_name' => $discount_name,
      'discount_component_title' => empty($component_title) ? t('Discount') : $component_title,
    ),
  );

  // Set the new unit price.
  $line_item_wrapper->commerce_unit_price->amount = $updated_amount;

  // Add the discount amount as a price component.
  $unit_price = commerce_price_wrapper_value($line_item_wrapper, 'commerce_unit_price', TRUE);
  $type = empty($component_title) ? 'discount' : check_plain('discount|' . $discount_name);
  $line_item_wrapper->commerce_unit_price->data = commerce_price_component_add($unit_price, $type, $difference, TRUE, TRUE);
}

/**
 * Rules options list callback for product match options.
 *
 * @return array
 *   Rules option list.
 */
function _commerce_discount_rules_product_match_options() {
  return array(
    'any' => t('any products'),
    'all' => t('all products'),
    'exactly' => t('exclusively all products'),
    'only' => t('exclusively any products'),
  );
}

/**
 * Sets a discount price component to the provided line item.
 *
 * @param EntityDrupalWrapper $line_item_wrapper
 *   The wrapped line item entity.
 * @param string $discount_name
 *   The name of the discount being applied.
 * @param array $discount_amount
 *   The discount amount price array (amount, currency_code).
 */
function commerce_discount_set_price_component(EntityDrupalWrapper $line_item_wrapper, $discount_name, $discount_amount) {
  $unit_price = commerce_price_wrapper_value($line_item_wrapper, 'commerce_unit_price', TRUE);
  $discount_wrapper = entity_metadata_wrapper('commerce_discount', $discount_name);
  $component_title = $discount_wrapper->component_title
    ->value();

  // Currencies don't match, abort.
  if ($discount_amount['currency_code'] != $unit_price['currency_code']) {
    return;
  }
  $discount_amount['amount'] = commerce_round(COMMERCE_ROUND_HALF_UP, $discount_amount['amount']);
  $discount_amount['data'] = array(
    'discount_name' => $discount_name,
    'discount_component_title' => empty($component_title) ? 'discount' : $component_title,
  );

  // Set the new unit price.
  $line_item_wrapper->commerce_unit_price->amount = $discount_amount['amount'];

  // Add the discount amount as a price component.
  $unit_price = commerce_price_wrapper_value($line_item_wrapper, 'commerce_unit_price', TRUE);
  $type = empty($component_title) ? 'discount' : check_plain('discount|' . $discount_name);
  $line_item_wrapper->commerce_unit_price->data = commerce_price_component_add($unit_price, $type, $discount_amount, TRUE, TRUE);
}

/**
 * Updates the unit price of the discount line item matching the named discount.
 *
 * Will be updated only first discount line item.
 * Non-discount line items are ignored.
 *
 * @param EntityDrupalWrapper $order_wrapper
 *   The wrapped order entity.
 * @param string $discount_name
 *   The name of the discount being applied.
 * @param array $discount_price
 *   The discount amount price array (amount, currency_code).
 *
 * @return object|bool
 *   The modified line item or FALSE if not found.
 */
function commerce_discount_set_existing_line_item_price(EntityDrupalWrapper $order_wrapper, $discount_name, $discount_price) {
  $modified_line_item = FALSE;
  foreach ($order_wrapper->commerce_line_items as $line_item_wrapper) {
    if ($line_item_wrapper
      ->getBundle() == 'commerce_discount') {

      // Add the discount component price if the line item was originally
      // added by discount module.
      $line_item = $line_item_wrapper
        ->value();
      if (isset($line_item->data['discount_name']) && $line_item->data['discount_name'] == $discount_name) {
        commerce_discount_set_price_component($line_item_wrapper, $discount_name, $discount_price);
        $line_item_wrapper
          ->save();
        $modified_line_item = $line_item;
        break;
      }
    }
  }
  return $modified_line_item;
}

/**
 * Options list callback for condition.
 */
function _commerce_discount_operator_options() {
  return array(
    '<' => t('less than'),
    '<=' => t('less than or equal to'),
    '==' => t('equals'),
    '>' => t('greater than'),
    '>=' => t('greater than or equal to'),
  );
}

/**
 * Rules condition callback: evaluate maximum usage per-person of a discount.
 */
function commerce_discount_usage_max_usage_per_person($order, $discount_name) {
  if (!($discount = entity_load_single('commerce_discount', $discount_name))) {
    return FALSE;
  }
  $per_person_limit = FALSE;

  // Don't use the wrapper getter on purpose for performance reasons.
  if (isset($discount->discount_usage_per_person[LANGUAGE_NONE])) {
    $per_person_limit = $discount->discount_usage_per_person[LANGUAGE_NONE][0]['value'];
  }

  // Nothing to count if the order does not have an email.
  if (!$per_person_limit || !$order->mail) {
    return TRUE;
  }

  // Find other orders owned by same person that have same discount.
  $usage = commerce_discount_usage_get_usage_by_mail($discount_name, $order->mail, $order->order_id);
  return $usage < $per_person_limit;
}

/**
 * Rules condition callback: evaluate maximum usage of a discount.
 */
function commerce_discount_usage_max_usage($order, $discount_name) {
  if (!($discount = entity_load_single('commerce_discount', $discount_name))) {
    return FALSE;
  }
  $limit = FALSE;

  // Don't use the wrapper getter on purpose for performance reasons.
  if (isset($discount->discount_usage_limit[LANGUAGE_NONE])) {
    $limit = $discount->discount_usage_limit[LANGUAGE_NONE][0]['value'];
  }
  if (!$limit) {
    return TRUE;
  }

  // Find other orders that have same discount.
  $usage = commerce_discount_usage_get_usage($discount_name, $order->order_id);
  return $usage < $limit;
}

/**
 * Rules condition: Check discount can be applied.
 */
function commerce_discount_date_condition($discount_name) {
  if (!($discount = entity_load_single('commerce_discount', $discount_name))) {
    return FALSE;
  }
  if (!isset($discount->commerce_discount_date[LANGUAGE_NONE])) {
    return TRUE;
  }
  $discount_date = $discount->commerce_discount_date[LANGUAGE_NONE][0];
  $time = time();
  $start_date = new DateTime();
  $start_date
    ->setTimestamp($discount_date['value']);
  $start_date
    ->setTime(0, 0, 0);
  $start_date_timestamp = $start_date
    ->getTimestamp();
  $end_date = new DateTime();
  $end_date
    ->setTimestamp($discount_date['value2']);
  $end_date
    ->setTime(24, 0, 0);
  $end_date_timestamp = $end_date
    ->getTimestamp();
  return $time >= $start_date_timestamp && $time <= $end_date_timestamp;
}

/**
 * Rules action: Remove discount components on product line items.
 */
function commerce_discount_remove_discount_components_on_products($line_item_wrapper) {

  // Skip unsaved line items.
  if (!$line_item_wrapper
    ->getIdentifier()) {
    return;
  }
  commerce_discount_remove_discount_components($line_item_wrapper->commerce_unit_price, array(
    'product_discount',
  ));
  commerce_discount_remove_discount_components($line_item_wrapper->commerce_total, array(
    'product_discount',
  ));
}

Functions

Namesort descending Description
commerce_discount_add_line_item Creates a discount line item on the provided order.
commerce_discount_add_price_component Adds a discount price component to the provided line item.
commerce_discount_compatibility_check Rules callback: executes the "Check discount compatibility at the order level" condition.
commerce_discount_date_condition Rules condition: Check discount can be applied.
commerce_discount_fixed_amount Rules action: Apply fixed amount discount.
commerce_discount_free_products Rules action: Apply free bonus products discount.
commerce_discount_line_item_added_by_discount Condition callback: looks to see if the line item was added by a discount.
commerce_discount_line_item_compatibility_check Rules callback: executes the "Check discount compatibility at the line item level" condition.
commerce_discount_percentage Rules action: Apply percentage discount.
commerce_discount_remove_discount_components_on_products Rules action: Remove discount components on product line items.
commerce_discount_rules_action_info Implements hook_rules_action_info().
commerce_discount_rules_condition_info Implements hook_rules_condition_info().
commerce_discount_rules_event_info Implements hook_rules_event_info().
commerce_discount_set_existing_line_item_price Updates the unit price of the discount line item matching the named discount.
commerce_discount_set_price_component Sets a discount price component to the provided line item.
commerce_discount_shipping_service Rules action: Apply shipping discount.
commerce_discount_usage_max_usage Rules condition callback: evaluate maximum usage of a discount.
commerce_discount_usage_max_usage_per_person Rules condition callback: evaluate maximum usage per-person of a discount.
commerce_order_compare_order_amount_build Build callback for commerce_order_compare_order_amount.
commerce_order_compare_order_amount_options_default Default value array for commerce_order_compare_order_amount line item types.
commerce_order_compare_order_amount_options_list Options list for commerce_order_compare_order_amount line item types.
commerce_order_contains_products_build Build callback for commerce_order_contains_products.
commerce_order_has_owner_build Build callback for commerce_order_has_owner.
commerce_order_has_specific_quantity_products_build Build callback for inline_conditions_product_quantity.
commerce_product_contains_products_build Build callback for commerce_product_contains_products.
commerce_product_has_specified_terms_build Build callback for commerce_product_has_specified_terms on product.
commerce_product_has_type_build Build callback for commerce_product_has_type.
_commerce_discount_add_shipping_discount_price_component Helper function for the shipping discount rules action.
_commerce_discount_check_compatibility Performs a discount compatibility check for the given price field.
_commerce_discount_operator_options Options list callback for condition.
_commerce_discount_rules_product_match_options Rules options list callback for product match options.