You are here

commerce_discount.module in Commerce Discount 7

Defines the discount and discount offer entities, bundles and functionality.

Discount and offer entities are always managed together, their bundles, and all surrounding functionality (API, UI).

File

commerce_discount.module
View source
<?php

/**
 * @file
 * Defines the discount and discount offer entities, bundles and functionality.
 *
 * Discount and offer entities are always managed together,
 * their bundles, and all surrounding functionality (API, UI).
 */

/**
 * Implements hook_commerce_cart_order_refresh().
 */
function commerce_discount_commerce_cart_order_refresh($order_wrapper) {

  // Remove all discount references from the order.
  // (If there are none, setting array() would modify the order, so test first.)
  if (isset($order_wrapper->commerce_discounts) && $order_wrapper->commerce_discounts
    ->value()) {
    $order_wrapper->commerce_discounts = array();
  }
  if (!isset($order_wrapper->commerce_line_items) || $order_wrapper->commerce_line_items
    ->count() <= 0) {
    return;
  }
  $line_items_to_delete = array();
  $currency_code = commerce_default_currency();
  if (!is_null($order_wrapper->commerce_order_total
    ->value())) {
    $currency_code = $order_wrapper->commerce_order_total->currency_code
      ->value();
  }

  // We create an empty price structure to be used by discount line items,
  // we want to remove all price components present on discount line items such
  // as VAT, the discount price component itself etc.
  $empty_price = commerce_discount_empty_price($currency_code);
  foreach ($order_wrapper->commerce_line_items as $delta => $line_item_wrapper) {
    if (!$line_item_wrapper
      ->value()) {
      continue;
    }

    // Remove all the price components on discount line items, if the unit price
    // is still == 0 after evaluating order discount rules, the discount line
    // item will be deleted.
    if ($line_item_wrapper
      ->getBundle() == 'commerce_discount') {
      $line_item_wrapper->commerce_unit_price = $empty_price;
      $line_item_wrapper->commerce_total = $empty_price;
      continue;
    }
    elseif ($line_item_wrapper
      ->getBundle() == 'product_discount') {
      $line_items_to_delete[] = $line_item_wrapper
        ->getIdentifier();
      $order_wrapper->commerce_line_items
        ->offsetUnset($delta);
      continue;
    }
    elseif ($line_item_wrapper
      ->getBundle() == 'shipping') {
      $changed = commerce_discount_remove_discount_components($line_item_wrapper->commerce_unit_price);
      if ($changed) {

        // Since we're saving the line item, there's no need to manually
        // update the line item's total since that'll be done in the controller.
        $line_item_wrapper
          ->save();
      }
    }
  }

  // We need to make sure the order total is correct before evaluating discount
  // rules, order total may not be correct when our refresh implementation is
  // invoked.
  commerce_discount_calculate_order_total($order_wrapper);

  // Re-add all applicable discount price components and/or line items.
  rules_invoke_event('commerce_discount_order', $order_wrapper);

  // After the discount rules were evaluated, we need to check if we have
  // some discount line items with empty prices (we need to delete them).
  foreach ($order_wrapper->commerce_line_items as $delta => $line_item_wrapper) {
    if (!$line_item_wrapper
      ->value() || $line_item_wrapper
      ->getBundle() != 'commerce_discount') {
      continue;
    }
    $unit_price = commerce_price_wrapper_value($line_item_wrapper, 'commerce_unit_price', TRUE);
    if (empty($unit_price['amount'])) {
      $line_items_to_delete[] = $line_item_wrapper
        ->getIdentifier();
      $order_wrapper->commerce_line_items
        ->offsetUnset($delta);
    }
  }

  // Delete discount line item if necessary.
  if ($line_items_to_delete) {
    commerce_line_item_delete_multiple($line_items_to_delete, TRUE);
  }
}

/**
 * Remove discount components from a given price and recalculate the total.
 *
 * @param object $price_wrapper
 *   Wrapped commerce price.
 * @param array $discount_types_to_remove
 *   An array of discount type strings to remove.
 *
 * @return bool
 *   TRUE if at least one price component has been removed, FALSE otherwise.
 */
function commerce_discount_remove_discount_components($price_wrapper, $discount_types_to_remove = array(
  'order_discount',
)) {
  $price = $price_wrapper
    ->value();

  // If there are no price or components, there is nothing to remove.
  if (!$price || empty($price['data']['components'])) {
    return FALSE;
  }
  $data = (array) $price['data'] + array(
    'components' => array(),
  );
  $component_removed = FALSE;

  // Remove price components belonging to order discounts.
  foreach ($data['components'] as $key => $component) {
    $remove = FALSE;

    // Remove all discount components.
    if (!empty($component['price']['data']['discount_name'])) {
      $discount_name = $component['price']['data']['discount_name'];
      $discount = entity_load_single('commerce_discount', $discount_name);
      if (!$discount || in_array($discount->type, $discount_types_to_remove)) {
        $remove = TRUE;
      }
    }
    elseif ($component['name'] != 'base_price') {

      // As long as the component is neither base price nor discount, allow
      // other modules to say whether it gets reset. This is so that discounts
      // can apply against a price that has been stripped of components from
      // modules that need to apply against a post-discount price.
      drupal_alter('commerce_discount_remove_price_component', $component, $remove);
    }
    if ($remove) {
      unset($data['components'][$key]);
      $component_removed = TRUE;
    }
  }

  // Don't alter the price components if no components were removed.
  if (!$component_removed) {
    return FALSE;
  }

  // Properly re-key the components array.
  $data['components'] = array_values($data['components']);

  // Re-save the price without the discounts (if existed).
  $price_wrapper->data
    ->set($data);

  // Re-set the total price.
  $total = commerce_price_component_total($price_wrapper
    ->value());
  $price_wrapper->amount = $total['amount'];
  return TRUE;
}

/**
 * Implements hook_commerce_cart_order_empty().
 */
function commerce_discount_commerce_cart_order_empty($order) {

  // Clean-up task to remove commerce_discount line items when cart is emptied.
  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
  $line_items_to_delete = array();
  foreach ($order_wrapper->commerce_line_items as $delta => $line_item_wrapper) {
    if ($line_item_wrapper
      ->getBundle() == 'commerce_discount') {
      $line_items_to_delete[] = $line_item_wrapper
        ->getIdentifier();
      $order_wrapper->commerce_line_items
        ->offsetUnset($delta);
    }
  }
  if ($line_items_to_delete) {

    // Delete line items.
    commerce_line_item_delete_multiple($line_items_to_delete);
  }
}

/**
 * Discount the shipping services for a given order.
 */
function commerce_discount_shipping_services($order) {
  commerce_cart_order_refresh($order);
}

/**
 * Implements hook_entity_info().
 */
function commerce_discount_entity_info() {
  $items['commerce_discount'] = array(
    'label' => t('Commerce Discount'),
    'controller class' => 'CommerceDiscountControllerExportable',
    'entity class' => 'CommerceDiscount',
    'base table' => 'commerce_discount',
    'fieldable' => TRUE,
    // For integration with Redirect module.
    // @see http://drupal.org/node/1263884
    'redirect' => FALSE,
    'exportable' => TRUE,
    'entity keys' => array(
      'id' => 'discount_id',
      'name' => 'name',
      'label' => 'label',
      'bundle' => 'type',
      'status' => 'export_status',
    ),
    'bundles' => array(),
    'module' => 'commerce_discount',
    'uri callback' => 'entity_class_uri',
    'access callback' => 'commerce_discount_access',
    'metadata controller class' => 'CommerceDiscountMetadataController',
    'views controller class' => 'CommerceDiscountViewsController',
    // Enable the entity API's admin UI.
    'admin ui' => array(
      'path' => 'admin/commerce/discounts',
      'file' => 'includes/commerce_discount.admin.inc',
      'controller class' => 'CommerceDiscountUIController',
    ),
  );
  foreach (commerce_discount_types() as $type => $info) {
    $items['commerce_discount']['bundles'][$type] = array(
      'label' => $info['label'],
    );
  }
  $items['commerce_discount_offer'] = array(
    'label' => t('Commerce Discount Offer'),
    'controller class' => 'EntityAPIControllerExportable',
    'entity class' => 'CommerceDiscountOffer',
    'base table' => 'commerce_discount_offer',
    'fieldable' => TRUE,
    'entity keys' => array(
      'id' => 'discount_offer_id',
      'bundle' => 'type',
    ),
    'bundles' => array(),
    'module' => 'commerce_discount',
    'metadata controller class' => 'EntityDefaultMetadataController',
    'inline entity form' => array(
      'controller' => 'CommerceDiscountOfferInlineEntityFormController',
    ),
  );
  foreach (commerce_discount_offer_types() as $type => $info) {
    $items['commerce_discount_offer']['bundles'][$type] = array(
      'label' => $info['label'],
    );
  }
  return $items;
}

/**
 * Implements hook_flush_caches().
 */
function commerce_discount_flush_caches() {
  module_load_install('commerce_discount');
  commerce_discount_install_helper();
}

/**
 * Implements hook_permission().
 */
function commerce_discount_permission() {
  $permissions = array();
  $permissions['administer commerce discounts'] = array(
    'title' => t('Administer discounts'),
  );
  return $permissions;
}

/**
 * Implements hook_views_api().
 */
function commerce_discount_views_api($module, $api) {
  return array(
    'version' => 3,
    'path' => drupal_get_path('module', 'commerce_discount') . '/includes/views',
  );
}

/**
 * Implements hook_features_pipe_commerce_discount_alter().
 *
 * Pipe the related Commerce discount order entity.
 */
function commerce_discount_features_pipe_commerce_discount_alter(&$pipe, $data, $export) {
  if (empty($data)) {
    return;
  }
  foreach ($data as $name) {
    $wrapper = entity_metadata_wrapper('commerce_discount', $name);
    $pipe['commerce_discount_offer'][] = $wrapper->commerce_discount_offer->type
      ->value();
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Hide the "Commerce discount offer" for the components list of features.
 */
function commerce_discount_form_features_export_form_alter(&$form, $from_state) {
  unset($form['export']['components']['#options']['commerce_discount_offer']);
}

/**
 * Implements hook_commerce_price_formatted_components_alter().
 */
function commerce_discount_commerce_price_formatted_components_alter(&$components, $price, $entity) {
  if (isset($price['data']['components'])) {

    // Loop into price components and alter the component title if the discount
    // component label is found.
    foreach ($price['data']['components'] as $component) {
      if (!isset($component['price']['data']['discount_component_title'])) {
        continue;
      }
      $components[$component['name']]['title'] = $component['price']['data']['discount_component_title'];
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Disable alteration of discounts with overridden rules.
 */
function commerce_discount_form_commerce_discount_form_alter(&$form, $form_state) {

  // Add clearfix to the discount type container.
  $form['commerce_discount_type']['#attributes']['class'][] = 'clearfix';
  $form['commerce_discount_fields']['commerce_discount_offer']['#attributes']['class'][] = 'clearfix';
  if (empty($form_state['commerce_discount']->discount_id)) {

    // Entity is new.
    return;
  }
  if (!empty($form_state['triggering_element']['#ajax'])) {

    // We are inside an Ajax call.
    return;
  }
  $rule = rules_config_load('commerce_discount_rule_' . $form_state['commerce_discount']->name);
  if ($rule && $rule
    ->hasStatus(ENTITY_OVERRIDDEN)) {
    drupal_set_message(t('The rule associated with this discount is overridden, making it impossible to edit the discount.'), 'warning');
    $form['actions']['submit']['#disabled'] = TRUE;
  }
}

/**
 * Implements hook_field_widget_form_alter().
 */
function commerce_discount_field_widget_form_alter(&$element, &$form_state, $context) {
  if (isset($element['#field_name']) && ($element['#field_name'] == 'commerce_free_shipping' || $element['#field_name'] == 'commerce_percent_off_ship_serv')) {
    $element['#options'] += commerce_shipping_service_options_list();
    if ($form_state['op'] == 'edit') {
      $wrapper_offer = entity_metadata_wrapper('commerce_discount_offer', $element['#entity']);
      if ($element['#field_name'] == 'commerce_free_shipping') {
        $element['#default_value'] = $wrapper_offer->commerce_free_shipping
          ->value();
      }
      else {
        $element['#default_value'] = $wrapper_offer->commerce_percent_off_ship_serv
          ->value();
      }
    }
  }

  // If the current element is the free shipping discount strategy select list,
  // alter it to only be visible when a free shipping service has been selected.
  if (!empty($element['#field_name']) && $element['#field_name'] == 'commerce_free_shipping_strategy') {
    $element['#states'] = array(
      'invisible' => array(
        ':input[name="commerce_discount_fields[commerce_discount_offer][' . LANGUAGE_NONE . '][form][commerce_free_shipping][' . LANGUAGE_NONE . ']"]' => array(
          'value' => '_none',
        ),
      ),
    );
  }
}

/**
 * Implements hook_commerce_discount_insert().
 *
 * Rebuild Rules configuration.
 */
function commerce_discount_commerce_discount_insert($entity) {
  if (module_exists('i18n_string')) {
    i18n_string_object_update('commerce_discount', $entity);
  }

  // We need to reload the discount afresh because some fields contain
  // serialized and empty values.
  $discount = entity_load_single('commerce_discount', $entity->discount_id);
  _commerce_discount_rebuild_rules_config(array(
    clone $discount,
  ));
}

/**
 * Implements hook_commerce_discount_update().
 *
 * Rebuild Rules configuration.
 */
function commerce_discount_commerce_discount_update($entity) {
  if (module_exists('i18n_string')) {

    // Account for name changes.
    if ($entity->original->name != $entity->name) {
      i18n_string_update_context("commerce_discount:commerce_discount:" . $entity->original->name . ":component_title", "commerce_discount:commerce_discount:" . $entity->name . ":component_title");
    }
    i18n_string_object_update('commerce_discount', $entity);
  }

  // We need to reload the discount afresh because some fields contain
  // serialized and empty values.
  $discount = entity_load_single('commerce_discount', $entity->discount_id);
  _commerce_discount_rebuild_rules_config(array(
    clone $discount,
  ));
}

/**
 * Actually rebuild the defaults of a given entity.
 *
 * @param array $discounts
 *   An array of discount entities.
 *
 * @see entity_defaults_rebuild()
 */
function _commerce_discount_rebuild_rules_config(array $discounts) {

  // Return early if we can't acquire a lock on the rules config.
  if (!lock_acquire('entity_rebuild_rules_config')) {
    return;
  }
  $existing_rules_config = FALSE;
  $rules_config_names = array();
  foreach ($discounts as $discount) {
    $rules_config_names[] = commerce_discount_build_rule_machine_name($discount->name);
  }
  $info = entity_get_info('rules_config');
  $hook = isset($info['export']['default hook']) ? $info['export']['default hook'] : 'default_rules_config';
  $keys = $info['entity keys'] + array(
    'module' => 'module',
    'status' => 'status',
    'name' => $info['entity keys']['id'],
  );

  // Check for the existence of the module and status columns.
  if (!in_array($keys['status'], $info['schema_fields_sql']['base table']) || !in_array($keys['module'], $info['schema_fields_sql']['base table'])) {
    trigger_error("Missing database columns for the exportable entity 'rules_config' as defined by entity_exportable_schema_fields(). Update the according module and run update.php!", E_USER_WARNING);
    return;
  }

  // Rebuild the discount rules for the given discounts.
  $entities = array();
  foreach (commerce_discount_build_discount_rules($discounts) as $name => $entity) {
    if (!in_array($name, $rules_config_names)) {
      continue;
    }
    $entity->{$keys['name']} = $name;
    $entity->{$keys['module']} = 'commerce_discount';
    $entities[$name] = $entity;
    $existing_rules_config = TRUE;
  }
  drupal_alter($hook, $entities);

  // Check for defaults that disappeared or overridden?
  if (!$existing_rules_config) {
    $statuses = array(
      $keys['status'] => array(
        ENTITY_OVERRIDDEN,
        ENTITY_IN_CODE,
        ENTITY_FIXED,
      ),
    );
    $existing_defaults = entity_load_multiple_by_name('rules_config', FALSE, $statuses);
    foreach ($existing_defaults as $name => $entity) {
      if (empty($entities[$name]) && in_array($name, $rules_config_names)) {
        $entity->is_rebuild = TRUE;
        if (entity_has_status('rules_config', $entity, ENTITY_OVERRIDDEN)) {
          $entity->{$keys['status']} = ENTITY_CUSTOM;
          entity_save('rules_config', $entity);
        }
        else {
          entity_delete('rules_config', $name);
        }
        unset($entity->is_rebuild);
      }
    }
  }

  // Load all existing entities.
  $existing_entities = entity_load_multiple_by_name('rules_config', array_keys($entities));
  foreach ($existing_entities as $name => $entity) {
    if (entity_has_status('rules_config', $entity, ENTITY_CUSTOM)) {

      // If the entity already exists but is not yet marked as overridden, we
      // have to update the status.
      if (!entity_has_status('rules_config', $entity, ENTITY_OVERRIDDEN)) {
        $entity->{$keys['status']} |= ENTITY_OVERRIDDEN;
        $entity->{$keys['module']} = $entities[$name]->{$keys['module']};
        $entity->is_rebuild = TRUE;
        entity_save('rules_config', $entity);
        unset($entity->is_rebuild);
      }

      // The entity is overridden, so we do not need to save the default.
      unset($entities[$name]);
    }
  }

  // Save defaults.
  $originals = array();
  foreach ($entities as $name => $entity) {
    if (!empty($existing_entities[$name])) {

      // Make sure we are updating the existing default.
      $entity->{$keys['id']} = $existing_entities[$name]->{$keys['id']};
      unset($entity->is_new);
    }

    // Pre-populate $entity->original as we already have it. So we avoid
    // loading it again.
    $entity->original = !empty($existing_entities[$name]) ? $existing_entities[$name] : FALSE;

    // Keep original entities for hook_{entity_type}_defaults_rebuild()
    // implementations.
    $originals[$name] = $entity->original;
    if (!isset($entity->{$keys['status']})) {
      $entity->{$keys['status']} = ENTITY_IN_CODE;
    }
    else {
      $entity->{$keys['status']} |= ENTITY_IN_CODE;
    }
    $entity->is_rebuild = TRUE;
    entity_save('rules_config', $entity);
    unset($entity->is_rebuild);
  }

  // Invoke an entity type-specific hook so modules may apply changes, e.g.
  // efficiently rebuild caches.
  module_invoke_all('rules_config_defaults_rebuild', $entities, $originals);
  lock_release('entity_rebuild_rules_config');
}

/**
 * Implements hook_commerce_discount_delete().
 *
 * Delete referenced commerce_discount_offer upon commerce_discount deletion.
 */
function commerce_discount_commerce_discount_delete($entity) {
  if (module_exists('i18n_string')) {
    i18n_string_object_remove('commerce_discount', $entity);
  }
  $wrapper = entity_metadata_wrapper('commerce_discount', $entity);

  // Delete the referenced commerce_discount_offer.
  if ($wrapper->commerce_discount_offer
    ->value()) {
    entity_delete('commerce_discount_offer', $wrapper->commerce_discount_offer
      ->getIdentifier());
  }
}

/**
 * Access callback for commerce_discount entities.
 */
function commerce_discount_access($op, $entity, $account, $entity_type) {
  return user_access('administer commerce discounts', $account);
}

/**
 * Implements hook_commerce_discount_type_info().
 */
function commerce_discount_commerce_discount_type_info() {
  $types = array();
  $types['order_discount'] = array(
    'label' => t('Order discount'),
    'event' => 'commerce_discount_order',
    'entity type' => 'commerce_order',
  );
  $types['product_discount'] = array(
    'label' => t('Product discount'),
    'event' => 'commerce_product_calculate_sell_price',
    // The line item of the product.
    'entity type' => 'commerce_line_item',
  );
  return $types;
}

/**
 * Implements hook_commerce_discount_offer_type_info().
 */
function commerce_discount_commerce_discount_offer_type_info() {
  $types = array();
  $types['fixed_amount'] = array(
    'label' => t('@currency off', array(
      '@currency' => commerce_currency_get_symbol(variable_get('commerce_default_currency', 'USD')),
    )),
    'action' => 'commerce_discount_fixed_amount',
    'entity types' => array(
      'commerce_order',
      'commerce_line_item',
    ),
  );
  $types['percentage'] = array(
    'label' => t('% off'),
    'action' => 'commerce_discount_percentage',
    'entity types' => array(
      'commerce_order',
      'commerce_line_item',
    ),
  );
  if (module_exists('commerce_shipping')) {
    $types['free_shipping'] = array(
      'label' => t('Free shipping'),
      'action' => 'commerce_discount_shipping_service',
      'entity types' => array(
        'commerce_order',
      ),
    );
    $types['percent_off_shipping'] = array(
      'label' => t('% off of shipping'),
      'action' => 'commerce_discount_shipping_service',
      'entity types' => array(
        'commerce_order',
      ),
    );
    $types['shipping_upgrade'] = array(
      'label' => t('Shipping service upgrade'),
      'action' => 'commerce_discount_shipping_service',
      'entity types' => array(
        'commerce_order',
      ),
    );
  }
  $types['free_products'] = array(
    'label' => t('Add free bonus products'),
    'action' => 'commerce_discount_free_products',
    'entity types' => array(
      'commerce_order',
    ),
  );
  return $types;
}

/**
 * Implements hook_commerce_discount_rule_build().
 */
function commerce_discount_commerce_discount_rule_build($rule, $discount) {
  $wrapper = entity_metadata_wrapper('commerce_discount', $discount);
  $discount_offer = $wrapper->commerce_discount_offer
    ->value();
  $wrapper_discount_offer = entity_metadata_wrapper('commerce_discount_offer', $discount_offer);

  // Check if property is attached to commerce free shipping!
  if (isset($wrapper_discount_offer->commerce_free_shipping)) {
    if (!($shipping_service = $wrapper_discount_offer->commerce_free_shipping
      ->value())) {

      // No need to change the rules event.
      return;
    }

    // Add missing parameter.
    foreach ($rule
      ->actions() as $action) {
      if ($action
        ->getElementName() == 'commerce_discount_free_shipping_service') {
        $action->settings['shipping_service'] = $shipping_service;
      }
    }
  }

  // Product level discounts must pass the line item's order.
  $map = array(
    'order_discount' => 'commerce-order',
    'product_discount' => 'commerce-line-item:order',
  );
  if (isset($map[$discount->type])) {

    // Add condition for per-person usage.
    if ($wrapper->discount_usage_per_person
      ->value()) {
      $rule
        ->condition('commerce_discount_usage_max_usage_per_person', array(
        'commerce_discount' => $discount->name,
        'order:select' => $map[$discount->type],
        'usage' => $wrapper->discount_usage_per_person
          ->value(),
      ));
    }

    // For normal usage.
    if ($wrapper->discount_usage_limit
      ->value()) {
      $rule
        ->condition('commerce_discount_usage_max_usage', array(
        'commerce_discount' => $discount->name,
        'order:select' => $map[$discount->type],
        'usage' => $wrapper->discount_usage_limit
          ->value(),
      ));
    }
  }
  if (!is_null($wrapper->commerce_discount_date
    ->value())) {

    // If the end date for the discount has already passed, disable the rule.
    // Note that we add a day to the discount end date. The date field value is
    // set for 12:00:00 AM on the end date, but we want the discount to remain
    // valid through that day.
    if (REQUEST_TIME >= $wrapper->commerce_discount_date->value2
      ->value() + 86400) {
      $rule->active = FALSE;
    }

    // Add condition to check usage didn't reach max uses.
    $rule
      ->condition('commerce_discount_date_condition', array(
      'commerce_discount' => $discount->name,
    ));
  }
}

/**
 * Return an array of all defined discount types.
 *
 * @return array
 *   The array of types, keyed by type name.
 */
function commerce_discount_types() {
  $discount_types =& drupal_static(__FUNCTION__);
  if (!isset($discount_types)) {
    $discount_types = array();
    foreach (module_implements('commerce_discount_type_info') as $module) {
      foreach (module_invoke($module, 'commerce_discount_type_info') as $type => $info) {
        $info += array(
          // Remember the providing module.
          'module' => $module,
        );
        $discount_types[$type] = $info;
      }
    }

    // Allow the type info to be altered by other modules.
    drupal_alter('commerce_discount_type_info', $discount_types);
  }
  return $discount_types;
}

/**
 * Loads the data for a specific discount type.
 *
 * @param string $discount_type
 *   The machine name of a discount type.
 *
 * @return mixed
 *   The requested array or FALSE if not found.
 */
function commerce_discount_type($discount_type) {
  $discount_types = commerce_discount_types();
  return isset($discount_types[$discount_type]) ? $discount_types[$discount_type] : FALSE;
}

/**
 * Returns the human readable name of any or all discount types.
 *
 * @param string $type
 *   Optional parameter specifying the type whose name to return.
 *
 * @return mixed
 *   Either an array of all discount type names keyed by the machine name or a
 *   string containing the human readable name for the specified type. If a type
 *   is specified that does not exist, this function returns FALSE.
 */
function commerce_discount_type_get_name($type = NULL) {
  $discount_types = commerce_discount_types();

  // Return a type name if specified and it exists.
  if (!empty($type)) {
    if (isset($discount_types[$type])) {
      return $discount_types[$type]['label'];
    }
    else {

      // Return FALSE if it does not exist.
      return FALSE;
    }
  }

  // Otherwise turn the array values into the type name only.
  foreach ($discount_types as $key => $value) {
    $discount_types[$key] = $value['label'];
  }
  return $discount_types;
}

/**
 * Return an array of all defined discount offer types.
 *
 * @return array
 *   The array of types, keyed by type name.
 */
function commerce_discount_offer_types() {
  $offer_types =& drupal_static(__FUNCTION__);
  if (!isset($offer_types)) {
    $offer_types = array();
    foreach (module_implements('commerce_discount_offer_type_info') as $module) {
      foreach (module_invoke($module, 'commerce_discount_offer_type_info') as $type => $info) {
        $info += array(
          // Remember the providing module.
          'module' => $module,
        );
        $offer_types[$type] = $info;
      }
    }

    // Allow the type info to be altered by other modules.
    drupal_alter('commerce_discount_offer_type_info', $offer_types);
  }
  return $offer_types;
}

/**
 * Loads the data for a specific discount offer type.
 *
 * @param string $offer_type
 *   The machine name of an offer type.
 *
 * @return mixed
 *   The requested array or FALSE if not found.
 */
function commerce_discount_offer_type($offer_type) {
  $offer_types = commerce_discount_offer_types();
  return isset($offer_types[$offer_type]) ? $offer_types[$offer_type] : FALSE;
}

/**
 * Returns the human readable name of any or all discount offer types.
 *
 * @param string $type
 *   Optional parameter specifying the offer type whose name to return.
 *
 * @return mixed
 *   Either an array of all discount offer type names keyed by the machine name
 *   or a string containing the human readable name for the specified type.
 *   If a type is specified that does not exist, this function returns FALSE.
 */
function commerce_discount_offer_type_get_name($type = NULL) {
  $offer_types = commerce_discount_offer_types();

  // Return a type name if specified and it exists.
  if (!empty($type)) {
    if (isset($offer_types[$type])) {
      return $offer_types[$type]['label'];
    }
    else {

      // Return FALSE if it does not exist.
      return FALSE;
    }
  }

  // Otherwise turn the array values into the type name only.
  foreach ($offer_types as $key => $value) {
    $offer_types[$key] = $value['label'];
  }
  return $offer_types;
}

/**
 * Return an array keyed by commerce discount name and label as value.
 */
function commerce_discount_entity_list() {
  $options = array();
  foreach (entity_load('commerce_discount') as $discount) {
    $options[$discount->name] = $discount->label;
  }
  return $options;
}

/**
 * Implements hook_commerce_line_item_type_info().
 */
function commerce_discount_commerce_line_item_type_info() {
  return array(
    'commerce_discount' => array(
      'type' => 'commerce_discount',
      'name' => t('Fixed amount discount'),
      'description' => t('Line item for fixed amounts.'),
      'add_form_submit_value' => t('Add discount'),
      'base' => 'commerce_discount_line_item',
      'callbacks' => array(
        'title' => 'commerce_discount_line_item_title',
      ),
    ),
    'product_discount' => array(
      'type' => 'product_discount',
      'name' => t('Product discounted'),
      'description' => t('References a discounted product.'),
      'product' => TRUE,
      'add_form_submit_value' => t('Add product'),
      'base' => 'commerce_product_line_item',
      'callbacks' => array(
        'title' => 'commerce_discount_product_line_item_title',
      ),
    ),
  );
}

/**
 * Determine the discount's line item title.
 *
 * @return string
 *   The line item title.
 */
function commerce_discount_line_item_title($line_item) {
  $discount = entity_load_single('commerce_discount', $line_item->data['discount_name']);
  if (is_object($discount) && !empty($discount->component_title)) {
    return check_plain($discount->component_title);
  }
  return t('Fixed amount discount');
}

/**
 * Determine the product_discount's line item title.
 *
 * This function wrap's the line item's title in a span element with a class to
 * provide stylistic changes to a product which has a discount.
 *
 * @return string
 *   The line item title.
 */
function commerce_discount_product_line_item_title($line_item) {
  return '<span class="line-item-title--has-discount">' . commerce_product_line_item_title($line_item) . '</span>';
}

/**
 * Returns an array of discount compatibility strategies.
 *
 * Returned array used in a radio buttons select list.
 */
function commerce_discount_compatibility_strategies() {
  return array(
    'any' => t('Any discount'),
    'except' => t('Any discount except specific discounts'),
    'only' => t('Only with specific discounts'),
    'none' => t('Not with any other discount'),
  );
}

/**
 * Returns the compatibility strategy for the given discount.
 *
 * @param CommerceDiscount $discount
 *   A Commerce Discount object with a commerce_compatibility_strategy field.
 *
 * @return string
 *   The discount's compatibility strategy or 'any' if none is set.
 */
function commerce_discount_get_compatibility_strategy($discount) {
  if (isset($discount->commerce_compatibility_strategy[LANGUAGE_NONE])) {
    $strategy = $discount->commerce_compatibility_strategy[LANGUAGE_NONE][0]['value'];
  }

  // If the compatibility strategy is not set, default to 'any'.
  if (empty($strategy)) {
    $strategy = 'any';
  }

  // Note that if the field is set to an unknown value, we still return it. The
  // default condition for evaluating discount compatibility will pass through
  // unknown discount strategies, so modules that set them are responsible also
  // to evaluate them on behalf of all discounts.
  return $strategy;
}

/**
 * Returns the compatibility selection for the given discount.
 *
 * @param CommerceDiscount $discount
 *   A Commerce Discount object with a commerce_compatibility_selection field.
 *
 * @return int[]
 *   An array of discount IDs selected for the discount's compatibility strategy
 *   or an empty array if none are selected (or a non-array value is found).
 */
function commerce_discount_get_compatibility_selection($discount) {

  // Don't use the entity_metadata_wrapper here on purpose for performance
  // reasons.
  $items = field_get_items('commerce_discount', $discount, 'commerce_compatibility_selection', LANGUAGE_NONE);
  $selection = array();
  if (!$items) {
    return $selection;
  }
  foreach ($items as $item) {
    $selection[] = $item['target_id'];
  }
  return $selection;
}

/**
 * Returns a list of discounts applied.
 *
 * Returned list used to make a particular price as determined by its
 * price components.
 *
 * @param array $price
 *   A price field value array including a data array with price components.
 * @param string $type
 *   Optional. A discount type to filter the return array by so that only
 *   discounts of a matching type are returned.
 *
 * @return array
 *   Array of applied discounts.
 */
function commerce_discount_get_discounts_applied_to_price($price, $type = '') {
  $applied_discounts = array();

  // Return early if the price has no components.
  if (empty($price['data']['components'])) {
    return array();
  }

  // Loop over the price components looking for known discount components.
  foreach ($price['data']['components'] as $component) {
    if (strpos($component['name'], 'discount|') === 0) {

      // Load the discount entity represented by this component.
      $applied_discount_name = substr($component['name'], 9);
      $applied_discount = entity_load_single('commerce_discount', $applied_discount_name);

      // Add it to the list of applied discounts keyed by ID to prevent
      // duplicates.
      if (!empty($applied_discount)) {

        // Only add the discount to the return value if type filtering was not
        // requested or the type matches.
        if (empty($type) || $applied_discount->type == $type) {
          $applied_discounts[$applied_discount->discount_id] = $applied_discount_name;
        }
      }
    }
  }
  return $applied_discounts;
}

/**
 * Returns a list of discount strategies for free shipping offers.
 */
function commerce_discount_free_shipping_strategies() {
  return array(
    'only_selected' => t('Only discount the selected shipping service.'),
    'discount_all' => t('Discount all other shipping services by the same amount as the selected shipping service.'),
  );
}

/**
 * Returns the free shipping strategy for the given discount.
 *
 * @param CommerceDiscount $discount
 *   A Commerce Discount object with a commerce_free_shipping_strategy field.
 *
 * @return string
 *   The discount's free shipping strategy or 'only_selected' if none is set.
 */
function commerce_discount_get_free_shipping_strategy($discount) {
  $discount_wrapper = entity_metadata_wrapper('commerce_discount', $discount);
  $strategy = $discount_wrapper->commerce_discount_offer->commerce_free_shipping_strategy
    ->value();

  // If the free shipping strategy is not set, default to 'only_selected'.
  if (empty($strategy)) {
    $strategy = 'only_selected';
  }

  // Note that if the field is set to an unknown value, we still return it. The
  // default action for applying a free shipping discount will pass through
  // unknown discount strategies, so modules that set them are responsible also
  // to evaluate them on behalf of all discounts.
  return $strategy;
}

/**
 * Get usage of a discount for a user, excluding a certain order id.
 *
 * @param string $discount_name
 *   The discount name.
 * @param string $mail
 *   The user mail.
 * @param int|bool $exclude_order_id
 *   If provided, the order_id to ignore when calculating the discount usage.
 *
 * @return int
 *   Return the number of usage by mail.
 */
function commerce_discount_usage_get_usage_by_mail($discount_name, $mail, $exclude_order_id = FALSE) {
  $usage =& drupal_static(__FUNCTION__, array());
  if (!isset($usage[$mail])) {
    $usage[$mail] = array();
    $query = db_select('commerce_discount_usage', 'g')
      ->fields('g')
      ->condition('g.mail', $mail);
    $results = $query
      ->execute()
      ->fetchAll(PDO::FETCH_ASSOC);
    foreach ($results as $result) {
      $usage[$mail] += array(
        $result['discount'] => array(),
      );
      $usage[$mail][$result['discount']][] = $result['order_id'];
    }
  }

  // The usage array for the given discount must be initialized if not set,
  // otherwise the array_diff will get a wrong argument.
  $usage[$mail] += array(
    $discount_name => array(),
  );

  // Exclude the order_id passed if necessary.
  if ($exclude_order_id) {
    $usage[$mail][$discount_name] = array_diff($usage[$mail][$discount_name], array(
      $exclude_order_id,
    ));
  }
  return count($usage[$mail][$discount_name]);
}

/**
 * Implements hook_commerce_order_update().
 */
function commerce_discount_commerce_order_update($order) {
  commerce_discount_usage_record_order_usage($order);
}

/**
 * Implements hook_commerce_order_insert().
 */
function commerce_discount_commerce_order_insert($order) {
  commerce_discount_usage_record_order_usage($order);
}

/**
 * Implements hook_commerce_order_delete().
 */
function commerce_discount_commerce_order_delete($order) {
  commerce_discount_usage_reset_order_usage($order);
}

/**
 * Loads all discounts connected to an order.
 *
 * Loads discounts including line item level discounts traced through
 * line item unit price components.
 *
 * @param object $order
 *   A fully qualified order object.
 *
 * @return array
 *   List of order discounts.
 */
function commerce_discount_usage_order_discounts($order) {
  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
  $order_discounts = array();
  foreach ($order_wrapper->commerce_line_items as $line_item_wrapper) {
    try {
      $unit_price = commerce_price_wrapper_value($line_item_wrapper, 'commerce_unit_price', TRUE);
    } catch (Exception $ex) {

      // Log and continue if we're unable to load the unit price.
      // @TODO Resolve how to prevent commerce_cart_order_refresh() from firing
      // inside an order save via entity_load_unchanged().
      // @see https://www.drupal.org/node/2661530
      $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS);
      $watchdog_variables = array(
        '@backtrace' => json_encode($backtrace, JSON_PRETTY_PRINT),
      );
      watchdog('commerce_discount', 'Discount usage line item is missing. Backtrace: <pre>@backtrace</pre>', $watchdog_variables, WATCHDOG_DEBUG);
      continue;
    }
    foreach ($unit_price['data']['components'] as $key => $component) {
      if (strpos($component['name'], 'discount|') === 0 && !empty($component['price']['data']['discount_name'])) {
        $order_discount_name = $component['price']['data']['discount_name'];
        $order_discount_wrapper = entity_metadata_wrapper('commerce_discount', $order_discount_name);

        // Make a list of discounts present via the order's line item price
        // components.
        if ($order_discount_wrapper
          ->value()) {
          $order_discounts[] = $order_discount_wrapper->name
            ->value();
        }
      }
    }
  }

  // Add the set of discounts directly referenced on the order.
  if (isset($order->commerce_discounts) && $order_wrapper->commerce_discounts
    ->value()) {
    foreach ($order_wrapper->commerce_discounts
      ->value() as $discount) {
      if (isset($discount->name)) {
        $order_discounts[] = $discount->name;
      }
    }
  }
  $order_discounts = array_unique($order_discounts);
  return $order_discounts;
}

/**
 * Record order usage.
 *
 * @param object $order
 *   A fully qualified order object.
 */
function commerce_discount_usage_record_order_usage($order) {

  // Reset usage for this order first.
  commerce_discount_usage_reset_order_usage($order);

  // Only record discount usage if the order has an email.
  $discount_names = commerce_discount_usage_order_discounts($order);
  foreach ($discount_names as $discount_name) {
    $record = array(
      'discount' => $discount_name,
      'mail' => $order->mail ? $order->mail : '',
      'order_id' => $order->order_id,
    );
    drupal_write_record('commerce_discount_usage', $record);
  }
}

/**
 * Reset usage statistics for an entire order.
 *
 * @param object $order
 *   A fully qualified order object.
 *
 * @return DeleteQuery
 *   Return a new DeleteQuery object.
 */
function commerce_discount_usage_reset_order_usage($order) {
  return db_delete('commerce_discount_usage')
    ->condition('order_id', $order->order_id)
    ->execute();
}

/**
 * Get usage of a discount, excluding a certain order id.
 *
 * @param string $discount_name
 *   The discount name.
 * @param bool $exclude_order_id
 *   If TRUE, the order id will be excluded from the SQL request.
 *
 * @return int
 *   Return the number of usage.
 */
function commerce_discount_usage_get_usage($discount_name, $exclude_order_id = FALSE) {
  $query = db_select('commerce_discount_usage', 'g')
    ->fields('g')
    ->condition('g.discount', $discount_name);
  if ($exclude_order_id) {
    $query
      ->condition('g.order_id', $exclude_order_id, '<>');
  }
  return (int) $query
    ->countQuery()
    ->execute()
    ->fetchField();
}

/**
 * Deletes the first discount line item on an order matching by discount name.
 *
 * @param EntityDrupalWrapper $order_wrapper
 *   The wrapped order entity.
 * @param string $discount_name
 *   The name of the discount whose line item should be deleted.
 *
 * @return bool
 *   TRUE if line item deleted, or FALSE if not found.
 */
function commerce_discount_delete_discount_line_item_by_name($order_wrapper, $discount_name) {
  foreach ($order_wrapper->commerce_line_items as $line_item_wrapper) {
    if ($line_item_wrapper
      ->getBundle() == 'commerce_discount') {
      $line_item = $line_item_wrapper
        ->value();
      if (isset($line_item->data['discount_name']) && $line_item->data['discount_name'] == $discount_name) {
        commerce_line_item_delete($line_item_wrapper->line_item_id
          ->value());
        return TRUE;
      }
    }
  }
  return FALSE;
}

/**
 * Implements hook_entity_info_alter().
 */
function commerce_discount_entity_info_alter(&$info) {
  if (module_exists('i18n_string')) {

    // Enable i18n support via the entity API.
    $info['commerce_discount']['i18n controller class'] = 'EntityDefaultI18nStringController';
  }
}

/**
 * Implements hook_entity_property_info_alter().
 */
function commerce_discount_entity_property_info_alter(&$info) {
  if (module_exists('i18n_string')) {

    // Mark some properties as translatable, but also denote that translation
    // works with i18n_string.
    foreach (array(
      'component_title',
    ) as $name) {
      $info['commerce_discount']['properties'][$name]['translatable'] = TRUE;
      $info['commerce_discount']['properties'][$name]['i18n string'] = TRUE;
    }
  }
}

/**
 * Implements hook_query_TAG_alter().
 */
function commerce_discount_query_entityreference_alter($query) {
  $field = $query
    ->getMetaData('field');

  // Alter the default autocomplete for the compatibility selection field...
  if ($field['field_name'] == 'commerce_compatibility_selection' && strpos(current_path(), 'entityreference/autocomplete') === 0) {

    // Extract the bundle of the discount whose compatibility settings are
    // being adjusted and limit the autocomplete query to discounts of the
    // same bundle (i.e. discount type, Product vs. Order).
    $bundle_name = arg(5);
    if (in_array($bundle_name, array(
      'product_discount',
      'order_discount',
    ))) {
      $query
        ->condition('type', $bundle_name);
    }

    // Also do not include the current discount itself as an option.
    $entity_id = arg(6);
    if ($entity_id != 'NULL') {
      $query
        ->condition('discount_id', $entity_id, '!=');
    }
  }
}

/**
 * Build the rules configuration for the given discounts.
 *
 * @param array $discounts
 *   An array of discount entities.
 *
 * @return array
 *   An array of rules configuration objects.
 */
function commerce_discount_build_discount_rules(array $discounts) {
  $rules = array();
  $types = commerce_discount_types();
  $offer_types = commerce_discount_offer_types();
  foreach ($discounts as $discount) {
    $wrapper = entity_metadata_wrapper('commerce_discount', $discount);
    $wrapper_properties = $wrapper
      ->getPropertyInfo();

    // Only for Commerce Discount wrappers with Commerce Discount Offer defined.
    if (isset($wrapper->commerce_discount_offer)) {
      $offer_bundle = $wrapper->commerce_discount_offer
        ->getBundle();
      if (!isset($offer_types[$offer_bundle])) {
        continue;
      }
      $type = $types[$discount->type];
      $offer_type = $offer_types[$offer_bundle];
      $rule = rules_reaction_rule();
      $rule->label = $discount->label;
      $rule->active = $discount->status;
      $rule->weight = !empty($discount->sort_order) ? $discount->sort_order - 11 : -1;
      $rule->tags = array(
        'Commerce Discount',
        check_plain($type['label']),
      );
      $rule
        ->event(!empty($offer_type['event']) ? $offer_type['event'] : $type['event'])
        ->action($offer_type['action'], array(
        'entity:select' => $type['entity type'],
        'commerce_discount' => $discount->name,
      ));

      // Add the compatibility condition to all discounts. Even if the current
      // discount is compatible with any other discount, we need to prevent it
      // from being added if a previously applied discount indicates it is not
      // compatible with the current one.
      if ($type['entity type'] == 'commerce_order') {
        $rule
          ->condition('commerce_discount_compatibility_check', array(
          'commerce_order:select' => 'commerce-order',
          'commerce_discount' => $discount->name,
        ));
      }
      else {
        $rule
          ->condition('commerce_discount_line_item_compatibility_check', array(
          'commerce_line_item:select' => 'commerce-line-item',
          'commerce_discount' => $discount->name,
        ));
      }

      // Let other modules alter the rule object, with configuration specific
      // to commerce discount. We don't invoke an alter function, as it can
      // be already achieved by implementing
      // hook_default_rules_configuration_alter().
      module_invoke_all('commerce_discount_rule_build', $rule, $discount);

      // Let inline_conditions fields add their own conditions.
      foreach ($wrapper_properties as $field_name => $property) {
        if (stripos($property['type'], 'inline_conditions') !== FALSE) {
          inline_conditions_build($rule, $wrapper->{$field_name}
            ->value());
        }
      }

      // Add the commerce discount to the rule configuration, so other may act
      // according to it, in hook_default_rules_configuration_alter().
      $rule->commerce_discount = $discount;
      $rule_machine_name = commerce_discount_build_rule_machine_name($discount->name);
      $rules[$rule_machine_name] = $rule;
    }
  }
  return $rules;
}

/**
 * Builds a machine name suitable for use as the rule machine name.
 *
 * @param string $discount_name
 *   The machine-name of the discount to build the rule name for.
 *
 * @return string
 *   A machine name to be used as the rule configuration machine name.
 */
function commerce_discount_build_rule_machine_name($discount_name) {
  $rule_machine_name = 'commerce_discount_rule_' . $discount_name;

  // Ensure the name isn't too long.
  if (strlen($rule_machine_name) > 64) {

    // Shorten the name but ensure uniqueness by using a hash.
    $hash = crc32($discount_name);
    $rule_machine_name = substr($rule_machine_name, 0, 64 - strlen($hash) - 1) . '_' . $hash;
  }
  return $rule_machine_name;
}

/**
 * Returns the default array structure for a price field for use when
 * reinitializing discount line items.
 *
 * @param string $currency_code
 *   The currency code.
 *
 * @return array
 *   An initialized price array.
 */
function commerce_discount_empty_price($currency_code) {
  $price = array(
    'amount' => 0,
    'currency_code' => $currency_code,
  );
  $price['data'] = commerce_price_component_add($price, 'base_price', $price, TRUE, FALSE);
  return $price;
}

/**
 * Dumb copy of commerce_order_calculate_total() which accepts a wrapped order.
 * This is necessary when called from within a function that already has a
 * wrapped order entity to make sure the static cache of the
 * commerce_order_total field is correctly updated.
 */
function commerce_discount_calculate_order_total($order_wrapper) {

  // First determine the currency to use for the order total.
  $default_currency_code = $currency_code = commerce_default_currency();
  $currencies = array();

  // Populate an array of how many line items on the order use each currency.
  foreach ($order_wrapper->commerce_line_items as $delta => $line_item_wrapper) {

    // If the current line item actually no longer exists...
    if (!$line_item_wrapper
      ->value()) {

      // Remove the reference from the order and continue to the next value.
      $order_wrapper->commerce_line_items
        ->offsetUnset($delta);
      continue;
    }
    $line_item_currency_code = $line_item_wrapper->commerce_total->currency_code
      ->value();
    if (!isset($currencies[$line_item_currency_code])) {
      $currencies[$line_item_currency_code] = 1;
    }
    else {
      $currencies[$line_item_currency_code]++;
    }
  }
  reset($currencies);

  // If only one currency is present on the order, use that to calculate the
  // order total.
  if (count($currencies) == 1) {
    $currency_code = key($currencies);
  }
  elseif (isset($currencies[$default_currency_code])) {

    // Otherwise use the site default currency if it's in the order.
    $currency_code = $default_currency_code;
  }
  elseif (count($currencies) > 1) {

    // Otherwise use the first currency on the order. We do this instead of
    // trying to determine the most dominant currency for now because using the
    // first currency leaves the option open for a UI based module to let
    // customers reorder the items in the cart by currency to get the order
    // total in a different currency. The currencies array still contains useful
    // data, though, should we decide to expand on the count by currency approach.
    $currency_code = key($currencies);
  }

  // Initialize the order total with the selected currency.
  $order_wrapper->commerce_order_total->amount = 0;
  $order_wrapper->commerce_order_total->currency_code = $currency_code;

  // Reset the data array of the order total field to only include a
  // base price component, set the currency code from any line item.
  $base_price = array(
    'amount' => 0,
    'currency_code' => $currency_code,
    'data' => array(),
  );
  $order_wrapper->commerce_order_total->data = commerce_price_component_add($base_price, 'base_price', $base_price, TRUE);
  $order_total = $order_wrapper->commerce_order_total
    ->value();

  // Then loop over each line item and add its total to the order total.
  $amount = 0;
  foreach ($order_wrapper->commerce_line_items as $delta => $line_item_wrapper) {

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

    // Add the totals.
    $amount += commerce_currency_convert($component_total['amount'], $component_total['currency_code'], $currency_code);

    // Combine the line item total's component prices into the order total.
    $order_total['data'] = commerce_price_components_combine($order_total, $line_item_total);
  }

  // Update the order total price field with the final total amount and data.
  $order_wrapper->commerce_order_total->amount = round($amount);
  $order_wrapper->commerce_order_total->data = $order_total['data'];
}

Functions

Namesort descending Description
commerce_discount_access Access callback for commerce_discount entities.
commerce_discount_build_discount_rules Build the rules configuration for the given discounts.
commerce_discount_build_rule_machine_name Builds a machine name suitable for use as the rule machine name.
commerce_discount_calculate_order_total Dumb copy of commerce_order_calculate_total() which accepts a wrapped order. This is necessary when called from within a function that already has a wrapped order entity to make sure the static cache of the commerce_order_total field is correctly updated.
commerce_discount_commerce_cart_order_empty Implements hook_commerce_cart_order_empty().
commerce_discount_commerce_cart_order_refresh Implements hook_commerce_cart_order_refresh().
commerce_discount_commerce_discount_delete Implements hook_commerce_discount_delete().
commerce_discount_commerce_discount_insert Implements hook_commerce_discount_insert().
commerce_discount_commerce_discount_offer_type_info Implements hook_commerce_discount_offer_type_info().
commerce_discount_commerce_discount_rule_build Implements hook_commerce_discount_rule_build().
commerce_discount_commerce_discount_type_info Implements hook_commerce_discount_type_info().
commerce_discount_commerce_discount_update Implements hook_commerce_discount_update().
commerce_discount_commerce_line_item_type_info Implements hook_commerce_line_item_type_info().
commerce_discount_commerce_order_delete Implements hook_commerce_order_delete().
commerce_discount_commerce_order_insert Implements hook_commerce_order_insert().
commerce_discount_commerce_order_update Implements hook_commerce_order_update().
commerce_discount_commerce_price_formatted_components_alter Implements hook_commerce_price_formatted_components_alter().
commerce_discount_compatibility_strategies Returns an array of discount compatibility strategies.
commerce_discount_delete_discount_line_item_by_name Deletes the first discount line item on an order matching by discount name.
commerce_discount_empty_price Returns the default array structure for a price field for use when reinitializing discount line items.
commerce_discount_entity_info Implements hook_entity_info().
commerce_discount_entity_info_alter Implements hook_entity_info_alter().
commerce_discount_entity_list Return an array keyed by commerce discount name and label as value.
commerce_discount_entity_property_info_alter Implements hook_entity_property_info_alter().
commerce_discount_features_pipe_commerce_discount_alter Implements hook_features_pipe_commerce_discount_alter().
commerce_discount_field_widget_form_alter Implements hook_field_widget_form_alter().
commerce_discount_flush_caches Implements hook_flush_caches().
commerce_discount_form_commerce_discount_form_alter Implements hook_form_FORM_ID_alter().
commerce_discount_form_features_export_form_alter Implements hook_form_FORM_ID_alter().
commerce_discount_free_shipping_strategies Returns a list of discount strategies for free shipping offers.
commerce_discount_get_compatibility_selection Returns the compatibility selection for the given discount.
commerce_discount_get_compatibility_strategy Returns the compatibility strategy for the given discount.
commerce_discount_get_discounts_applied_to_price Returns a list of discounts applied.
commerce_discount_get_free_shipping_strategy Returns the free shipping strategy for the given discount.
commerce_discount_line_item_title Determine the discount's line item title.
commerce_discount_offer_type Loads the data for a specific discount offer type.
commerce_discount_offer_types Return an array of all defined discount offer types.
commerce_discount_offer_type_get_name Returns the human readable name of any or all discount offer types.
commerce_discount_permission Implements hook_permission().
commerce_discount_product_line_item_title Determine the product_discount's line item title.
commerce_discount_query_entityreference_alter Implements hook_query_TAG_alter().
commerce_discount_remove_discount_components Remove discount components from a given price and recalculate the total.
commerce_discount_shipping_services Discount the shipping services for a given order.
commerce_discount_type Loads the data for a specific discount type.
commerce_discount_types Return an array of all defined discount types.
commerce_discount_type_get_name Returns the human readable name of any or all discount types.
commerce_discount_usage_get_usage Get usage of a discount, excluding a certain order id.
commerce_discount_usage_get_usage_by_mail Get usage of a discount for a user, excluding a certain order id.
commerce_discount_usage_order_discounts Loads all discounts connected to an order.
commerce_discount_usage_record_order_usage Record order usage.
commerce_discount_usage_reset_order_usage Reset usage statistics for an entire order.
commerce_discount_views_api Implements hook_views_api().
_commerce_discount_rebuild_rules_config Actually rebuild the defaults of a given entity.