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.moduleView 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'];
}