You are here

commerce_avatax.module in Drupal Commerce Connector for AvaTax 7.5

AvaTax service integration from Avalara, Inc.

File

commerce_avatax.module
View source
<?php

/**
 * @file
 * AvaTax service integration from Avalara, Inc.
 */
define('COMMERCE_AVATAX_PRODUCTION_MODE', 'prod');
define('COMMERCE_AVATAX_DEVELOPMENT_MODE', 'dev');
define('COMMERCE_AVATAX_VAR_PREFIX', 'commerce_avatax_');

// Defines constants for the field names.
define('COMMERCE_AVATAX_ACCOUNT_NUMBER', 'account_number');
define('COMMERCE_AVATAX_LICENSE_KEY', 'license_key');
define('COMMERCE_AVATAX_COMPANY_CODE', 'company_code');
define('COMMERCE_AVATAX_TAX_CODE_FIELD', 'commerce_avatax_code');
define('COMMERCE_AVATAX_EXEMPTION_CODE_FIELD', 'commerce_avatax_exemption_code');
define('COMMERCE_AVATAX_VAT_ID_FIELD', 'commerce_avatax_vat_id');

/**
 * Implements hook_page_alter().
 */
function commerce_avatax_page_alter() {
  $path = current_path();
  if (module_exists('commerce_tax') && strpos($path, 'admin/commerce/config/taxes') === 0) {
    drupal_set_message(t('Please disable Commerce Tax module to avoid duplicate sales tax line items. Configure Commerce AvaTax !here.', array(
      '!here' => l(t('here'), 'admin/commerce/config/avatax'),
    )));
  }
}

/**
 * Implements hook_permission().
 */
function commerce_avatax_permission() {
  return array(
    'administer avatax' => array(
      'title' => t('Administer Avatax'),
      'description' => t('Allows users to configure Commerce AvaTax'),
    ),
    'configure avatax exemptions' => array(
      'title' => t('Configure Tax exemptions'),
      'description' => t('Allow users to configure tax exemptions'),
    ),
  );
}

/**
 * Implements hook_field_access().
 */
function commerce_avatax_field_access($op, $field, $entity_type, $entity, $account) {
  if ($field['field_name'] == 'commerce_avatax_exemption_code' && $op == 'edit') {
    return user_access('configure avatax exemptions', $account);
  }
}

/**
 * Implements hook_module_implements_alter().
 */
function commerce_avatax_module_implements_alter(&$implementations, $hook) {

  // Place this module's implementation of hook_form_alter() at the
  // end of the invocation in order to ensure our custom submit function
  // added to the checkout submit function runs last (once the customer
  // profile is saved).
  if (in_array($hook, array(
    'form_alter',
    'commerce_cart_order_refresh',
  )) && isset($implementations['commerce_avatax'])) {
    $group = $implementations['commerce_avatax'];
    unset($implementations['commerce_avatax']);
    $implementations['commerce_avatax'] = $group;
  }
}

/**
 * Implements hook_menu().
 */
function commerce_avatax_menu() {
  $items['admin/commerce/config/avatax'] = array(
    'title' => 'Avatax ',
    'description' => 'Avatax Configuration',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'commerce_avatax_credentials_settings_form',
    ),
    'file' => 'includes/commerce_avatax.admin.inc',
    'access arguments' => array(
      'administer avatax',
    ),
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/commerce/config/avatax/credentials'] = array(
    'title' => 'Credentials',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
    'access arguments' => array(
      'administer avatax',
    ),
    'weight' => -10,
  );
  $items['admin/commerce/config/avatax/shipping-settings'] = array(
    'title' => 'Shipping settings',
    'type' => MENU_LOCAL_TASK,
    'access arguments' => array(
      'administer avatax',
    ),
    'page arguments' => array(
      'commerce_avatax_shipping_settings_form',
    ),
    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
    'weight' => -1,
  );
  $items['admin/commerce/config/avatax/address-validation'] = array(
    'title' => 'Address validation',
    'type' => MENU_LOCAL_TASK,
    'access arguments' => array(
      'administer avatax',
    ),
    'page arguments' => array(
      'commerce_avatax_address_settings_form',
    ),
    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
  );
  $items['admin/commerce/config/avatax/general-settings'] = array(
    'title' => 'General settings',
    'type' => MENU_LOCAL_TASK,
    'access arguments' => array(
      'administer avatax',
    ),
    'page arguments' => array(
      'commerce_avatax_general_settings_form',
    ),
    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
  );
  $items['admin/commerce/config/avatax/global-vat'] = array(
    'title' => 'Global VAT',
    'type' => MENU_LOCAL_TASK,
    'access arguments' => array(
      'administer avatax',
    ),
    'page arguments' => array(
      'commerce_avatax_global_vat_settings_form',
    ),
    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
    'weight' => 10,
  );
  $items['admin/commerce/orders/%commerce_order/edit/calculate-tax'] = array(
    'title' => 'Calculate Taxes',
    'description' => 'Call the AvaTax service in order to calculate & apply the Tax amount.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'commerce_avatax_order_admin_calculate_tax_form',
      3,
    ),
    'access callback' => 'commerce_avatax_order_admin_form_access',
    'access arguments' => array(
      3,
    ),
    'type' => MENU_LOCAL_ACTION,
    'file' => 'includes/commerce_avatax.admin.inc',
  );
  return $items;
}

/**
 * Access callback: determines access to the "Calculate Tax" local action.
 */
function commerce_avatax_order_admin_form_access($order) {
  $company_code = commerce_avatax_company_code();
  $order_is_cart = module_exists('commerce_cart') && commerce_cart_order_is_cart($order);

  // Skip tax calculation if the option is disabled, or if the company code
  // is empty & skip cart orders.
  if (!commerce_avatax_tax_calculation_enabled() || empty($company_code) || $order_is_cart) {
    return FALSE;
  }
  if (empty($order->commerce_line_items)) {
    return FALSE;
  }
  return commerce_order_access('update', $order);
}

/**
 * Returns the API mode.
 */
function commerce_avatax_api_mode() {
  return variable_get(COMMERCE_AVATAX_VAR_PREFIX . 'api_mode', COMMERCE_AVATAX_DEVELOPMENT_MODE);
}

/**
 * Returns the site-wide AvaTax account number.
 */
function commerce_avatax_account_number() {
  return variable_get(COMMERCE_AVATAX_VAR_PREFIX . commerce_avatax_api_mode() . '_' . COMMERCE_AVATAX_ACCOUNT_NUMBER, '');
}

/**
 * Returns the site-wide AvaTax company code for a given API mode.
 */
function commerce_avatax_company_code() {
  return variable_get(COMMERCE_AVATAX_VAR_PREFIX . commerce_avatax_api_mode() . '_' . COMMERCE_AVATAX_COMPANY_CODE, '');
}

/**
 * Returns the site-wide AvaTax license key.
 */
function commerce_avatax_license_key() {
  return variable_get(COMMERCE_AVATAX_VAR_PREFIX . commerce_avatax_api_mode() . '_' . COMMERCE_AVATAX_LICENSE_KEY, '');
}

/**
 * Determines if the tax calculation is enabled.
 */
function commerce_avatax_tax_calculation_enabled() {
  return variable_get(COMMERCE_AVATAX_VAR_PREFIX . 'tax_calculation_enabled', TRUE);
}

/**
 * Returns a statically cached instance of an Avatax object.
 *
 * @param string $api_key
 *   The API key to use to submit requests to the Avatax API.
 * @param string $api_mode
 *   The passed API module will determine the base url of the API.
 *
 * @return Avatax|bool.
 *   The constructed Avatax object or FALSE if the library could not be loaded..
 */
function commerce_avatax_object($api_key = '', $api_mode = '') {
  $avatax =& drupal_static(__FUNCTION__, array());

  // If the Api key wasn't provided.
  if (empty($api_key)) {
    $account_number = commerce_avatax_account_number();
    $license_key = commerce_avatax_license_key();
    if (!empty($account_number) && !empty($license_key)) {
      $api_key = base64_encode("{$account_number}:{$license_key}");
    }
    else {
      return FALSE;
    }
  }
  if (!isset($avatax[$api_key])) {
    $logger = NULL;
    $api_mode = empty($api_mode) ? commerce_avatax_api_mode() : $api_mode;

    // Specify the logger if the logging was enabled.
    if (variable_get(COMMERCE_AVATAX_VAR_PREFIX . 'enable_logging', FALSE)) {
      $logger = 'watchdog';
    }

    // Specify the x-Avalara-Client header.
    $server_machine_name = gethostname();
    $module_info = system_get_info('module', 'commerce_avatax');
    $version = !empty($module_info['version']) ? $module_info['version'] : '5.x';
    $headers = array(
      "x-Avalara-Client" => "Drupal Commerce; Version [{$version}]; REST; V2; [{$server_machine_name}]",
    );
    $avatax[$api_key] = new Avatax($api_key, $api_mode, $logger, $headers);
  }
  return $avatax[$api_key];
}

/**
 * Implements hook_commerce_line_item_type_info().
 */
function commerce_avatax_commerce_line_item_type_info() {
  $line_item_types = array();
  $line_item_types['avatax'] = array(
    'name' => t('AvaTax'),
    'description' => t('Tax calculated by AvaTax.'),
    'add_form_submit_value' => t('Add Tax'),
    'base' => 'commerce_avatax_line_item',
  );
  return $line_item_types;
}

/**
 * Implements hook_commerce_price_component_type_info().
 */
function commerce_avatax_commerce_price_component_type_info() {
  $components = array();

  // Define a generic AvaTax price component type.
  $components['avatax_sales_tax'] = array(
    'title' => t('AvaTax sales tax'),
    'display_title' => t('Sales tax'),
    'weight' => 25,
  );

  // Define a price component for VAT.
  $components['avatax_vat'] = array(
    'title' => t('AvaTax VAT'),
    'display_title' => t('VAT'),
    'weight' => 25,
  );
  return $components;
}

/**
 * Returns the title of an AvaTax line item.
 */
function commerce_avatax_line_item_title($line_item) {
  return !empty($line_item->line_item_label) ? $line_item->line_item_label : t('Tax');
}

/**
 * Implements hook_field_widget_form_alter().
 */
function commerce_avatax_field_widget_form_alter(&$element, &$form_state, $context) {
  if ($context['instance']['widget']['type'] == 'commerce_line_item_manager') {
    foreach ($element['line_items'] as $line_item_id => $line_item) {
      if ($line_item['line_item']['#value']->type == 'avatax') {
        $element['line_items'][$line_item_id]['commerce_unit_price']['#access'] = FALSE;
        $element['line_items'][$line_item_id]['quantity']['#access'] = FALSE;
      }
    }
  }
}

/**
 * Creates a new AvaTax line item populated with the proper values.
 *
 * @param array $tax_price
 *   A price array used to initialize the value of the line item's unit price.
 * @param string $tax_type
 *   A string determining the price component to add.
 * @param int $order_id
 *   The ID of the order the line item belongs to.
 * @param array $data
 *   An array value to initialize the line item's data array with.
 *
 * @return
 *   The AvaTax line item initialized to the given unit price.
 */
function commerce_avatax_line_item_new($tax_price, $tax_type = 'sales_tax', $order_id = 0, $data = array()) {
  $types_mapping = array(
    'sales_tax' => array(
      'line_item_label' => t('Sales Tax'),
      'price_component' => 'avatax_sales_tax',
    ),
    'vat' => array(
      'line_item_label' => t('VAT'),
      'price_component' => 'avatax_vat',
    ),
  );
  if (isset($types_mapping[$tax_type])) {
    $line_item_label = $types_mapping[$tax_type]['line_item_label'];
    $price_component = $types_mapping[$tax_type]['price_component'];
  }
  else {

    // Defaults to Sales Tax.
    $line_item_label = t('Sales Tax');
    $price_component = $types_mapping['sales_tax']['price_component'];
  }

  // Create the new line item.
  $line_item = entity_create('commerce_line_item', array(
    'type' => 'avatax',
    'order_id' => $order_id,
    'quantity' => 1,
    'line_item_label' => $line_item_label,
    'data' => $data,
  ));

  // Set the unit price.
  $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
  $line_item_wrapper->commerce_unit_price->amount = $tax_price['amount'];
  $line_item_wrapper->commerce_unit_price->currency_code = $tax_price['currency_code'];

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

  // Return the line item.
  return $line_item;
}

/**
 * Implements hook_commerce_cart_order_refresh().
 *
 * Calculate taxes on order refresh in order to properly take in account
 * discounts.
 */
function commerce_avatax_commerce_cart_order_refresh($order_wrapper) {

  // Skip the request if there are no line items.
  if ($order_wrapper->commerce_line_items
    ->count() === 0) {
    return;
  }
  commerce_avatax_calculate_tax($order_wrapper);
}

/**
 * Performs Tax calculation for a given order.
 *
 * @param EntityDrupalWrapper $order_wrapper
 *    The wrapped order entity.
 *
 * @return array|bool
 *   An associative array containing the request & the response, FALSE in case
 *   the request could not be performed.
 */
function commerce_avatax_calculate_tax($order_wrapper) {

  // Skip tax calculation if the option is disabled, or if the company code
  // is empty.
  if (!commerce_avatax_tax_calculation_enabled()) {
    return FALSE;
  }
  $company_code = commerce_avatax_company_code();

  // If the company code is not configured, return FALSE.
  if (empty($company_code) || !($avatax_object = commerce_avatax_object())) {
    drupal_set_message(t("The Avatax module is not properly configured, please configure the company code."), 'error');
    return FALSE;
  }
  $order = $order_wrapper
    ->value();

  // Get the customer profile to use for tax calculation.
  $field_name = commerce_avatax_get_customer_profile_field();

  // Checks if the Sales Tax needs to be calculated for this address.
  if (!$field_name || !commerce_avatax_check_address($order_wrapper, $field_name)) {

    // Delete any existing AvaTax line items from the order.
    commerce_avatax_delete_tax_line_items($order_wrapper, TRUE);

    // Remove the stored request & response from the order's data.
    $order->data['commerce_avatax'] = array();
    return FALSE;
  }
  module_load_include('inc', 'commerce_avatax', 'includes/commerce_avatax.calc');
  $stored_request = array();

  // Retrieve the stored request in the order's data array,
  // compare it with the request that's about to be sent, and skip it if
  // unnecessary.
  if (isset($order->data['commerce_avatax']['request'])) {
    $stored_request = $order->data['commerce_avatax']['request'];
  }

  // Prepare the request.
  $request = commerce_avatax_create_transaction($order_wrapper, 'SalesOrder');

  // In order for the request comparison to work properly,
  // make sure the "date" is the same in both arrays.
  if (isset($stored_request['date']) && isset($request['date'])) {
    $stored_request['date'] = $request['date'];
  }

  // Stop here if the stored request in the order's object is similar to
  // the one we're about to perform (i.e no need to perform an unnecessary
  // request.
  if ($stored_request == $request) {
    return array(
      'request' => $request,
      'response' => $order->data['commerce_avatax']['response'] ? $order->data['commerce_avatax']['response'] : array(),
    );
  }

  // Remove the AvaTax data from the order.
  $order->data['commerce_avatax'] = array();

  // Only perform the request the request array contains a "lines" key.
  if (empty($request['lines'])) {

    // Delete the Tax line item if present.
    commerce_avatax_delete_tax_line_items($order_wrapper, TRUE);
    return FALSE;
  }
  $response = $avatax_object
    ->transactionsCreate($request);
  if (empty($response['success']) || !isset($response['result']['totalTax'])) {

    // Delete the Tax line item if present.
    commerce_avatax_delete_tax_line_items($order_wrapper, TRUE);
    return FALSE;
  }

  // Store the request & the response in the order's data.
  $order->data['commerce_avatax'] = array(
    'request' => $request,
    'response' => $response,
  );

  // Parse the result request.
  $result = $response['result'];
  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
  $currency_code = $order_wrapper->commerce_order_total->currency_code
    ->value();
  $tax_price = array(
    'amount' => commerce_currency_decimal_to_amount($result['totalTax'], $currency_code),
    'currency_code' => $currency_code,
    'data' => array(),
  );

  // Assume VAT when the customer's country is not in the US|CA.
  $tax_type = 'sales_tax';
  if (isset($order_wrapper->{$field_name})) {
    $customer_address = $order_wrapper->{$field_name}->commerce_customer_address
      ->value();
    if (!in_array($customer_address['country'], array(
      'US',
      'CA',
    ))) {
      $tax_type = 'vat';
    }
  }

  // Modify the existing tax line item or add a new one if that fails.
  if (!commerce_avatax_set_existing_line_item_price($order_wrapper, $tax_price, $tax_type)) {
    commerce_avatax_add_line_item($order_wrapper, $tax_price, $tax_type);
  }

  // Update the total order price, for the next rules condition (if any).
  commerce_order_calculate_total($order);
  return array(
    'request' => $request,
    'response' => $response,
  );
}

/**
 * Updates the unit price of the Avatax line item if it exists.
 *
 * @param EntityDrupalWrapper $order_wrapper
 *   The wrapped order entity.
 * @param array $tax_price
 *   A price array used to initialize the value of the line item's unit price.
 * @param string $tax_type
 *   A string determining the price component to add.
 * @return object|bool
 *   The tax line item wrapper or FALSE if not found.
 */
function commerce_avatax_set_existing_line_item_price(EntityDrupalWrapper $order_wrapper, $tax_price, $tax_type) {
  foreach ($order_wrapper->commerce_line_items as $line_item_wrapper) {
    if (!$line_item_wrapper
      ->value() || $line_item_wrapper
      ->getBundle() != 'avatax') {
      continue;
    }
    commerce_avatax_set_price_component($line_item_wrapper, $tax_price, $tax_type);
    $line_item_wrapper
      ->save();
    return $line_item_wrapper;
  }
  return FALSE;
}

/**
 * Create, add an AvaTax line item to an order, and saves the order.
 *
 * @param EntityDrupalWrapper $order_wrapper
 *   The wrapped order entity.
 * @param array $tax_price
 *   A price array used to initialize the value of the line item's unit price.
 * @param string $tax_type
 *   A string determining the price component to add.
 *
 * @return object
 *   The newly created tax line item wrapper.
 */
function commerce_avatax_add_line_item(EntityDrupalWrapper $order_wrapper, $tax_price, $tax_type) {

  // Create the new line item.
  $line_item = entity_create('commerce_line_item', array(
    'type' => 'avatax',
    'order_id' => $order_wrapper
      ->getIdentifier(),
    'quantity' => 1,
  ));

  // Sets the unit price.
  $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
  commerce_avatax_set_price_component($line_item_wrapper, $tax_price, $tax_type);

  // Save the incoming line item now so we get its ID.
  commerce_line_item_save($line_item);

  // Add it to the order's line item reference value.
  $order_wrapper->commerce_line_items[] = $line_item;

  // Return the line item.
  return $line_item_wrapper;
}

/**
 * Sets a tax price component to the provided line item.
 *
 * @param $line_item_wrapper
 *   An entity_metadata_wrapper() for the line item whose unit price should be
 *     used in the vat calculation.
 * @param array $tax_price
 *   A price array used to initialize the value of the line item's unit price.
 * @param string $tax_type
 *   A string determining the price component to add.
 */
function commerce_avatax_set_price_component($line_item_wrapper, $tax_price, $tax_type) {
  $types_mapping = array(
    'sales_tax' => array(
      'line_item_label' => t('Sales Tax'),
      'price_component' => 'avatax_sales_tax',
    ),
    'vat' => array(
      'line_item_label' => t('VAT'),
      'price_component' => 'avatax_vat',
    ),
  );
  if (isset($types_mapping[$tax_type])) {
    $line_item_label = $types_mapping[$tax_type]['line_item_label'];
    $price_component = $types_mapping[$tax_type]['price_component'];
  }
  else {

    // Defaults to Sales Tax.
    $line_item_label = t('Sales Tax');
    $price_component = $types_mapping['sales_tax']['price_component'];
  }
  $line_item_wrapper->line_item_label = $line_item_label;
  $line_item_wrapper->commerce_unit_price->amount = $tax_price['amount'];
  $line_item_wrapper->commerce_unit_price->currency_code = $tax_price['currency_code'];

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

/**
 * Checks if the Avatax service needs to be called for this address.
 */
function commerce_avatax_check_address(EntityDrupalWrapper $order_wrapper, $field_name) {

  // Skip the check if the customer profile field is empty.
  if (!isset($order_wrapper->{$field_name}) || is_null($order_wrapper->{$field_name}
    ->value())) {
    return FALSE;
  }
  if (!isset($order_wrapper->{$field_name}->commerce_customer_address)) {
    return FALSE;
  }
  $customer_address = $order_wrapper->{$field_name}->commerce_customer_address
    ->value();

  // Don't calculate Sales Tax if the provided address is in the US but the
  // State is not in Avax states list.
  if ($customer_address['country'] == 'US') {
    $avatax_states = variable_get(COMMERCE_AVATAX_VAR_PREFIX . 'select_states', array());

    // Exit if not a valid AvaTax state.
    if (!empty($avatax_states) && !in_array($customer_address['administrative_area'], $avatax_states)) {
      return FALSE;
    }
  }
  return TRUE;
}

/**
 * Deletes AvaTax line items of an order.
 *
 * @param EntityDrupalWrapper $order_wrapper
 *   The wrapped order entity.
 * @param $skip_save
 *   Boolean indicating whether or not to skip saving the order in this function.
 */
function commerce_avatax_delete_tax_line_items(EntityDrupalWrapper $order_wrapper, $skip_save = FALSE) {
  $line_items_to_delete = array();
  foreach ($order_wrapper->commerce_line_items as $delta => $line_item_wrapper) {

    // Skip non Avatax line items.
    if (!$line_item_wrapper
      ->value() || $line_item_wrapper
      ->getBundle() != 'avatax') {
      continue;
    }
    $line_items_to_delete[] = $line_item_wrapper->line_item_id
      ->value();
    $order_wrapper->commerce_line_items
      ->offsetUnset($delta);
  }

  // If we found line items to delete.
  if (!empty($line_items_to_delete)) {

    // First save the order to update the line item reference field value.
    if (!$skip_save) {
      $order_wrapper
        ->save();
    }

    // Delete the line items on shutdown, to prevent the line item controller
    // delete method to fire and save the order for us.
    drupal_register_shutdown_function('commerce_line_item_delete_multiple', $line_items_to_delete);
  }
}

/**
 * Generate AvaTax user name as approximation of e-mail address.
 */
function commerce_avatax_email_to_username($user_email) {

  // Default to the first part of the e-mail address.
  $name = substr($user_email, 0, strpos($user_email, '@'));

  // Remove possible illegal characters.
  $name = preg_replace('/[^A-Za-z0-9_.-]/', '', $name);

  // Trim that value for spaces and length.
  $name = trim(substr($name, 0, USERNAME_MAX_LENGTH - 4));
  return $name;
}

/**
 * Implements hook_commerce_payment_order_paid_in_full().
 *
 * Create a committed SalesInvoice transaction when an order is paid in full.
 */
function commerce_avatax_commerce_payment_order_paid_in_full($order) {
  $company_code = commerce_avatax_company_code();

  // Skip the transaction creation.
  if (!commerce_avatax_tax_calculation_enabled() || empty($company_code) || empty($order->data['commerce_avatax']['request'])) {
    return;
  }

  // Check if there's an AvaTax line item present on this order.
  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
  $tax_line_item = FALSE;
  foreach ($order_wrapper->commerce_line_items as $delta => $line_item_wrapper) {
    if (!$line_item_wrapper
      ->value() || $line_item_wrapper
      ->getBundle() != 'avatax') {
      continue;
    }
    $tax_line_item = $line_item_wrapper
      ->value();
    break;
  }

  // If the tax line item could not be found, stop here.
  if (!$tax_line_item) {
    return;
  }

  // Commit the transaction when "Disable document committing" is unchecked.
  module_load_include('inc', 'commerce_avatax', 'includes/commerce_avatax.calc');
  if (empty($company_code) || !($avatax_object = commerce_avatax_object())) {
    drupal_set_message(t("The Avatax module is not properly configured, please configure the company code."), 'error');
    return;
  }

  // Reuse the stored AvaTax transaction from the line item's data array.
  $request_body = $order->data['commerce_avatax']['request'];

  // Check if the transaction needs to be committed.
  $commit = !variable_get(COMMERCE_AVATAX_VAR_PREFIX . 'disable_document_committing', FALSE);

  // Update the transaction type if it needs to be committed.
  if ($commit) {
    $request_body['type'] = 'SalesInvoice';
  }
  $request_body['commit'] = $commit;
  $response = $avatax_object
    ->transactionsCreate($request_body);

  // Parse the result request.
  if ($response['success'] && isset($response['result']['id'])) {
    $order->data['commerce_avatax'] = array(
      'transaction_id' => $response['result']['id'],
      'transaction_code' => $response['result']['code'],
      // Stores the company code, it might change overtime.
      'company_code' => $company_code,
      'request' => $request_body,
      'response' => $response,
    );
  }
}

/**
 * COMMIT an existing transaction for a given $order.
 */
function commerce_avatax_commit_transaction($order) {
  if (isset($order->data['commerce_avatax']['transaction_code']) && ($avatax = commerce_avatax_object())) {
    if (!empty($order->data['commerce_avatax']['company_code'])) {
      $avatax
        ->transactionsCommit($order->data['commerce_avatax']['company_code'], $order->data['commerce_avatax']['transaction_code']);
    }
  }
}

/**
 * VOID AvaTax transaction for $order.
 */
function commerce_avatax_void_transaction($order, $code = 'DocDeleted') {
  if (isset($order->data['commerce_avatax']['transaction_code']) && ($avatax = commerce_avatax_object())) {
    if (!empty($order->data['commerce_avatax']['company_code'])) {
      $parameters = array(
        'code' => $code,
      );
      $avatax
        ->transactionsVoid($order->data['commerce_avatax']['company_code'], $order->data['commerce_avatax']['transaction_code'], $parameters);
    }
  }
}

/**
 * Implements hook_form_alter().
 *
 * Validate addresses during checkout if necessary.
 */
function commerce_avatax_form_alter(&$form, &$form_state, $form_id) {

  // If we're dealing with a commerce checkout form.
  if (strpos($form_id, 'commerce_checkout_form_') === 0) {
    $address_validation_enabled = variable_get(COMMERCE_AVATAX_VAR_PREFIX . 'validate_address', TRUE);

    // Only alter the checkout form when the Address validation is enabled.
    if (!$address_validation_enabled) {
      return;
    }
    $checkout_page_id = substr($form_id, 23);
    $customer_profile_pane_found = FALSE;

    // Find all panes for our current checkout page.
    foreach (commerce_checkout_panes(array(
      'enabled' => TRUE,
      'page' => $checkout_page_id,
    )) as $pane_id => $checkout_pane) {

      // If this pane is a customer profile based pane build a list of previous
      // profiles from which to pick that are of the same bundle.
      if (substr($pane_id, 0, 16) == 'customer_profile' && isset($form[$pane_id])) {
        $customer_profile_pane_found = TRUE;

        // HACK: We need to add a custom element validate handler if the profile
        // copy checkbox is present.
        // The profile copy element validate code will check if the triggering
        // element is the "continue" button.
        // However, in case the address validation button is present, it would
        // be the triggering element making the profile copy validation to fail.
        if (isset($form[$pane_id]['commerce_customer_profile_copy'])) {
          array_unshift($form[$pane_id]['commerce_customer_profile_copy']['#element_validate'], 'commerce_avatax_profile_copy_validate');
          break;
        }
      }
    }

    // If there are customer profiles pane on that page, we need to perform
    // Address validation.
    if ($customer_profile_pane_found) {
      $form['#attached']['library'][] = array(
        'system',
        'ui.dialog',
      );
      $module_path = drupal_get_path('module', 'commerce_avatax');
      $form['#attached']['js'][] = $module_path . '/js/commerce_avatax.js';
      $form['#attached']['css'][] = $module_path . '/css/commerce_avatax.checkout.css';
      $form['address_validation_result'] = array(
        '#type' => 'container',
        '#id' => 'commerce-avatax-address-validation-wrapper',
      );

      // Since we're preventing the default form submit, we need our custom
      // button to call the checkout continue validate functions.
      $validate_handlers = $form['buttons']['continue']['#validate'];
      $validate_handlers[] = 'commerce_avatax_checkout_validate';
      array_unshift($form['buttons']['continue']['#submit'], 'commerce_avatax_address_suggestion_apply');

      // This submit button will be clicked when the main form is submitted.
      $form['buttons']['validate-address'] = array(
        '#value' => t('Validate address'),
        '#type' => 'submit',
        '#attributes' => array(
          'class' => array(
            'element-invisible',
          ),
        ),
        '#id' => 'commerce-avatax-address-validate-btn',
        '#validate' => $validate_handlers,
        '#ajax' => array(
          'callback' => 'commerce_avatax_validate_shipping_address_ajax_callback',
          'progress' => array(
            'type' => 'none',
          ),
        ),
      );

      // Store the address suggestion delta in order to use it.
      $form['use_suggested_address'] = array(
        '#type' => 'hidden',
      );
    }
  }
}

/**
 * Element validate callback for the profile copy checkbox.
 */
function commerce_avatax_profile_copy_validate($element, &$form_state, $form) {
  $triggering_element = end($form_state['triggering_element']['#array_parents']);

  // HACK: Fool the profile copy checkbox by making it believe the checkout
  // continue button was clicked.
  if ($triggering_element == 'validate-address') {
    array_pop($form_state['triggering_element']['#array_parents']);
    $form_state['triggering_element']['#array_parents'][] = 'continue';
  }
}

/**
 * Submit handler for the continue button of the checkout form.
 * Apply the address suggestion if selected in the Jquery Dialog.
 */
function commerce_avatax_address_suggestion_apply($form, &$form_state) {

  // Check if we need to apply the selected address suggestion.
  if (!isset($form_state['commerce_avatax']) || empty($form_state['values']['use_suggested_address'])) {
    return;
  }
  $settings = $form_state['commerce_avatax'];
  $profile = commerce_customer_profile_load($settings['customer_profile_to_update']);

  // If the profile could not be loaded, or if no address suggestion was found.
  if (!is_object($profile) || !isset($settings['address_validation_result']['suggestions'])) {
    return;
  }
  $address_suggestion = reset($settings['address_validation_result']['suggestions']);
  $profile_wrapper = entity_metadata_wrapper('commerce_customer_profile', $profile);
  foreach ($address_suggestion as $key => $value) {
    $profile_wrapper->commerce_customer_address->{$key} = $value;
  }
  $profile_wrapper
    ->save();

  // If the profile ID has been updated, we need to update the order's
  // reference.
  if ($settings['customer_profile_to_update'] != $profile->profile_id) {
    $pane_id = 'customer_profile_' . $profile->type;
    if ($field_name = variable_get('commerce_' . $pane_id . '_field', '')) {
      $order = commerce_order_load($form_state['order']->order_id);
      $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
      $order_wrapper->{$field_name} = $profile->profile_id;
      commerce_order_save($order);
    }
  }
}

/**
 * Custom Ajax callback for setting up address validation popup.
 */
function commerce_avatax_validate_shipping_address_ajax_callback($form, &$form_state) {
  $commands = array();

  // See commerce_avatax_checkout_validate().
  if (isset($form_state['commerce_avatax']) && !empty($form_state['commerce_avatax']['address_validation_failed'])) {
    if (!empty($form_state['commerce_avatax']['address_validation_result'])) {
      $validation_result = $form_state['commerce_avatax']['address_validation_result'];
      $buttons = array();
      if ($validation_result['result'] == 'invalid') {
        $buttons[] = array(
          'code' => 'invalid',
          'text' => t('Let me change the address'),
        );
      }
      elseif ($validation_result['result'] == 'needs_correction') {
        $buttons[] = array(
          'code' => 'recommended',
          'text' => t('Use recommended'),
        );
        $buttons[] = array(
          'code' => 'keep_address',
          'text' => t('Use as entered'),
        );
        $buttons[] = array(
          'code' => 'invalid',
          'text' => t('Let me change the address'),
        );
      }
      $commands[] = array(
        'command' => 'commerce_avatax_address_modal_display',
        'html' => $validation_result['msg'],
        'buttons' => $buttons,
        'selector' => '#commerce-avatax-address-validation-wrapper',
      );
    }
  }
  else {

    // We need to unblock the form submission.
    $commands[] = ajax_command_invoke(NULL, 'commerceAvaTaxUnblockCheckout', array());
  }
  return array(
    '#type' => 'ajax',
    '#commands' => $commands,
  );
}

/**
 * Checkout form validation callback.
 */
function commerce_avatax_checkout_validate($form, &$form_state) {

  // If the rebuild flag is set to TRUE by Commerce Checkout skip the address
  // validation.
  if (!empty($form_state['rebuild'])) {
    return;
  }
  $form_state['commerce_avatax'] = array();
  $field_name = commerce_avatax_get_customer_profile_field();

  // Skip the address validation if we couldn't find the field.
  if (!$field_name || !($field = field_info_field($field_name))) {
    return;
  }
  $profile_type = $field['settings']['profile_type'];
  $pane_id = 'customer_profile_' . $profile_type;

  // Addressbook 3.x integration
  // Check if the profile can be found in the $form_state object.
  if (isset($form_state['pane_' . $pane_id]['profile'])) {
    $profile_wrapper = entity_metadata_wrapper('commerce_customer_profile', $form_state['pane_' . $pane_id]['profile']);
  }
  else {

    // The form state order object is stale, we need to reload it.
    $order = commerce_order_load($form_state['order']->order_id);
    $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
    $profile_wrapper = $order_wrapper->{$field_name};
  }
  if (!isset($profile_wrapper->commerce_customer_address)) {
    return;
  }
  $address = $profile_wrapper->commerce_customer_address
    ->value();
  $country = $address['country'];
  $enabled_countries = variable_get(COMMERCE_AVATAX_VAR_PREFIX . 'address_validate_countries', array(
    'US',
  ));
  if (!in_array($country, $enabled_countries)) {
    return;
  }

  // Include the address validation functions.
  module_load_include('inc', 'commerce_avatax', 'includes/commerce_avatax.address');
  $validated_addresses = commerce_avatax_validate_address($address);
  if ($validated_addresses === NULL) {
    return;
  }
  $result = commerce_avatax_address_compare($address, $validated_addresses);
  if ($result['result'] != 'valid') {
    $form_state['commerce_avatax']['address_validation_failed'] = TRUE;
    $form_state['commerce_avatax']['address_validation_result'] = $result;

    // Store the customer profile ID to update.
    $form_state['commerce_avatax']['customer_profile_to_update'] = $profile_wrapper
      ->getIdentifier();
    $form_state['rebuild'] = TRUE;
  }
}

/**
 * Returns address suggestion form.
 */
function commerce_avatax_address_suggestion_form($form, &$form_state, $original_address, $suggestions) {
  if (count($suggestions) == 1) {
    $form['info'] = array(
      '#type' => 'markup',
      '#markup' => '<p>' . t('Your shipping address is different from the post office records. We suggest you accept the recommended address to avoid shipping delays.') . '</p>',
    );
  }
  else {
    $form['info'] = array(
      '#type' => 'markup',
      '#markup' => '<p>' . t('Your shipping address is different from the post office records. We suggest you accept one of the recommended addresses to avoid shipping delays.') . '</p>',
    );
  }
  $form['original_address'] = array(
    '#type' => 'markup',
    '#markup' => '<p>' . t('Entered address is:') . '</p>' . theme('commerce_avatax_address', array(
      'address' => $original_address,
    )),
  );
  $options = array();
  foreach ($suggestions as $key => $address) {
    $options[$key] = theme('commerce_avatax_address', array(
      'address' => $address,
    ));
  }
  $form['addresses'] = array(
    '#title' => t('Recommended address'),
    '#type' => 'radios',
    '#options' => $options,
    '#default_value' => '0',
  );
  return $form;
}

/**
 * Implements hook_theme().
 */
function commerce_avatax_theme() {
  return array(
    'commerce_avatax_address' => array(
      'variables' => array(
        'address' => array(),
      ),
    ),
  );
}

/**
 * Returns a mapping between the Addressfield names and the field names that
 * need to be sent to the Avatax API v1.
 */
function commerce_avatax_address_fields_mapping($flip = FALSE) {
  $mapping = array(
    'locality' => 'city',
    'administrative_area' => 'region',
    'country' => 'country',
    'postal_code' => 'postalCode',
    'thoroughfare' => 'line1',
    'premise' => 'line2',
  );

  // Flip the keys/values if necessary.
  if ($flip) {
    $mapping = array_flip($mapping);
  }
  return $mapping;
}

/**
 * Format address array to be used in the address suggestion form.
 */
function theme_commerce_avatax_address($variables) {
  $address = $variables['address'];
  $components = array(
    $address['thoroughfare'],
  );
  if (!empty($address['premise'])) {
    $components[] = $address['premise'];
  }
  $components[] = $address['locality'];
  $components[] = $address['administrative_area'] . ' ' . $address['postal_code'];
  $components[] = $address['country'];
  return implode('<br/>', $components);
}

/**
 * Implements hook_flush_caches().
 */
function commerce_avatax_flush_caches() {
  module_load_install('commerce_avatax');
  commerce_avatax_install_helper();
}

/**
 * Allowed values callback for exemption codes.
 */
function commerce_avatax_exemption_codes_allowed_values() {
  return array(
    'A' => 'Federal government (United States)',
    'B' => 'State government (United States)',
    'C' => 'Tribe / Status Indian / Indian Band',
    'D' => 'Foreign diplomat',
    'E' => 'Charitable or benevolent org',
    'F' => 'Religious or educational org',
    'G' => 'Resale',
    'H' => 'Commercial agricultural production',
    'I' => 'Industrial production / manufacturer',
    'J' => 'Direct pay permit (United States)',
    'K' => 'Direct mail (United States)',
    'L' => 'Other',
    'N' => 'Local government (United States)',
    'P' => 'Commercial aquaculture (Canada)',
    'Q' => 'Commercial Fishery (Canada)',
    'R' => 'Non-resident (Canada)',
  );
}

/**
 * Options list callback for the commerce_avatax_void_transaction() rules
 * condition.
 */
function commerce_avatax_void_codes_list() {
  return drupal_map_assoc(array(
    'Unspecified',
    'PostFailed',
    'DocDeleted',
    'DocVoided',
    'AdjustmentCancelled',
  ));
}

/**
 * Returns the configured customer profile field to use.
 */
function commerce_avatax_get_customer_profile_field() {
  $customer_profile_to_use = variable_get(COMMERCE_AVATAX_VAR_PREFIX . 'tax_address', 'shipping');
  $profile_types = commerce_customer_profile_types();

  // Fallback to the first customer profile type available.
  if (!isset($profile_types[$customer_profile_to_use])) {
    $customer_profile_to_use = key($profile_types);
  }
  $pane_id = 'customer_profile_' . $customer_profile_to_use;
  if ($field_name = variable_get('commerce_' . $pane_id . '_field', '')) {
    return $field_name;
  }
  return FALSE;
}

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

  // Clean-up task to remove avatax 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() == 'avatax') {
      $line_items_to_delete[] = $line_item_wrapper
        ->getIdentifier();
      $order_wrapper->commerce_line_items
        ->offsetUnset($delta);
    }
  }

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

Functions

Namesort descending Description
commerce_avatax_account_number Returns the site-wide AvaTax account number.
commerce_avatax_address_fields_mapping Returns a mapping between the Addressfield names and the field names that need to be sent to the Avatax API v1.
commerce_avatax_address_suggestion_apply Submit handler for the continue button of the checkout form. Apply the address suggestion if selected in the Jquery Dialog.
commerce_avatax_address_suggestion_form Returns address suggestion form.
commerce_avatax_add_line_item Create, add an AvaTax line item to an order, and saves the order.
commerce_avatax_api_mode Returns the API mode.
commerce_avatax_calculate_tax Performs Tax calculation for a given order.
commerce_avatax_checkout_validate Checkout form validation callback.
commerce_avatax_check_address Checks if the Avatax service needs to be called for this address.
commerce_avatax_commerce_cart_order_empty Implements hook_commerce_cart_order_empty().
commerce_avatax_commerce_cart_order_refresh Implements hook_commerce_cart_order_refresh().
commerce_avatax_commerce_line_item_type_info Implements hook_commerce_line_item_type_info().
commerce_avatax_commerce_payment_order_paid_in_full Implements hook_commerce_payment_order_paid_in_full().
commerce_avatax_commerce_price_component_type_info Implements hook_commerce_price_component_type_info().
commerce_avatax_commit_transaction COMMIT an existing transaction for a given $order.
commerce_avatax_company_code Returns the site-wide AvaTax company code for a given API mode.
commerce_avatax_delete_tax_line_items Deletes AvaTax line items of an order.
commerce_avatax_email_to_username Generate AvaTax user name as approximation of e-mail address.
commerce_avatax_exemption_codes_allowed_values Allowed values callback for exemption codes.
commerce_avatax_field_access Implements hook_field_access().
commerce_avatax_field_widget_form_alter Implements hook_field_widget_form_alter().
commerce_avatax_flush_caches Implements hook_flush_caches().
commerce_avatax_form_alter Implements hook_form_alter().
commerce_avatax_get_customer_profile_field Returns the configured customer profile field to use.
commerce_avatax_license_key Returns the site-wide AvaTax license key.
commerce_avatax_line_item_new Creates a new AvaTax line item populated with the proper values.
commerce_avatax_line_item_title Returns the title of an AvaTax line item.
commerce_avatax_menu Implements hook_menu().
commerce_avatax_module_implements_alter Implements hook_module_implements_alter().
commerce_avatax_object Returns a statically cached instance of an Avatax object.
commerce_avatax_order_admin_form_access Access callback: determines access to the "Calculate Tax" local action.
commerce_avatax_page_alter Implements hook_page_alter().
commerce_avatax_permission Implements hook_permission().
commerce_avatax_profile_copy_validate Element validate callback for the profile copy checkbox.
commerce_avatax_set_existing_line_item_price Updates the unit price of the Avatax line item if it exists.
commerce_avatax_set_price_component Sets a tax price component to the provided line item.
commerce_avatax_tax_calculation_enabled Determines if the tax calculation is enabled.
commerce_avatax_theme Implements hook_theme().
commerce_avatax_validate_shipping_address_ajax_callback Custom Ajax callback for setting up address validation popup.
commerce_avatax_void_codes_list Options list callback for the commerce_avatax_void_transaction() rules condition.
commerce_avatax_void_transaction VOID AvaTax transaction for $order.
theme_commerce_avatax_address Format address array to be used in the address suggestion form.

Constants