<?php

/**
 * @file
 * Processes payments using Authorize.net. Supports AIM and ARB.
 *
 * Development sponsored by Digital Dollhouse - http://www.digitaldollhouse.com
 */

define('UC_AUTHORIZENET_TEST_GATEWAY_URL', 'https://test.authorize.net/gateway/transact.dll');
define('UC_AUTHORIZENET_LIVE_GATEWAY_URL', 'https://secure.authorize.net/gateway/transact.dll');

/**
 * Implements hook_menu().
 */
function uc_authorizenet_menu() {
  $items = array();

  $items['authnet/silent-post'] = array(
    'page callback' => 'uc_authorizenet_silent_post',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
    'file' => 'uc_authorizenet.pages.inc',
  );

  // User operations menu items for ARB recurring fees.
  $items['user/%user/recurring/%/arb-update'] = array(
    'title' => 'Update your payment details',
    'description' => 'Update the payment details for a recurring fee.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('uc_authorizenet_arb_user_update_form', 1, 3),
    'access callback' => 'uc_recurring_user_access',
    'access arguments' => array(1, 3),
    'type' => MENU_CALLBACK,
    'file' => 'uc_authorizenet.pages.inc',
  );
  $items['user/%user/recurring/%/arb-cancel'] = array(
    'title' => 'Cancel the recurring fee?',
    'description' => 'Cancel a recurring fee.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('uc_authorizenet_arb_user_cancel_form', 1, 3),
    'access callback' => 'uc_recurring_user_access',
    'access arguments' => array(1, 3),
    'type' => MENU_CALLBACK,
    'file' => 'uc_authorizenet.pages.inc',
  );

  // Admin operations menu items.
  $items['admin/store/orders/recurring/%/arb-update'] = array(
    'title' => 'Update ARB subscription',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('uc_authorizenet_arb_admin_update_form', 4),
    'access arguments' => array('administer recurring fees'),
    'type' => MENU_CALLBACK,
    'file' => 'uc_authorizenet.admin.inc',
  );
  $items['admin/store/orders/recurring/%/arb-cancel'] = array(
    'title' => 'Cancel ARB subscription',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('uc_authorizenet_arb_admin_cancel_form', 4),
    'access arguments' => array('administer recurring fees'),
    'type' => MENU_CALLBACK,
    'file' => 'uc_authorizenet.admin.inc',
  );

  return $items;
}

/**
 * Implements hook_uc_payment_gateway().
 */
function uc_authorizenet_uc_payment_gateway() {
  $gateways['authorizenet'] = array(
    'title' => t('Authorize.net'),
    'description' => t('Process credit card payments using the AIM service of Authorize.net.'),
    'settings' => 'uc_authorizenet_settings_form',
    'credit' => 'uc_authorizenet_charge',
    'credit_txn_types' => array(UC_CREDIT_AUTH_ONLY, UC_CREDIT_PRIOR_AUTH_CAPTURE, UC_CREDIT_AUTH_CAPTURE, UC_CREDIT_REFERENCE_SET, UC_CREDIT_REFERENCE_TXN),
  );

  return $gateways;
}

/**
 * Callback for payment gateway settings.
 */
function uc_authorizenet_settings_form($form, &$form_state) {
  $login_data = _uc_authorizenet_login_data();

  // Allow admin to set duplicate window.
  $form['uc_authnet_duplicate_window'] = array(
    '#type' => 'select',
    '#title' => t('Duplicate window'),
    '#description' => t('Blocks submission of duplicate transactions within the specified window.  Defaults to 120 seconds.'),
    '#default_value' => variable_get('uc_authnet_duplicate_window', 120),
    '#options' => drupal_map_assoc(array(0, 15, 30, 45, 60, 75, 90, 105, 120)),
  );

  $form['api_id_key'] = array(
    '#type' => 'fieldset',
    '#title' => t('API Login ID and Transaction Key'),
    '#description' => t('This information is required for Ubercart to interact with your payment gateway account.  It is different from your login ID and password and may be found through your account settings page.  Do not change the gateway URLs unless you are using this module with an Authorize.net-compatible gateway that requires different URLs.'),
  );
  $form['api_id_key']['uc_authnet_api_login_id'] = array(
    '#type' => 'textfield',
    '#title' => t('API Login ID'),
    '#default_value' => variable_get('uc_authnet_api_login_id', ''),
  );
  $form['api_id_key']['uc_authnet_api_transaction_key'] = array(
    '#type' => 'textfield',
    '#title' => t('Transaction Key'),
    '#default_value' => variable_get('uc_authnet_api_transaction_key', ''),
  );
  $form['api_id_key']['uc_authnet_api_test_gateway_url'] = array(
    '#type' => 'textfield',
    '#title' => t('Authorize.net Test Gateway URL'),
    '#default_value' => variable_get('uc_authnet_api_test_gateway_url', UC_AUTHORIZENET_TEST_GATEWAY_URL),
  );
  $form['api_id_key']['uc_authnet_api_live_gateway_url'] = array(
    '#type' => 'textfield',
    '#title' => t('Authorize.net Live Gateway URL'),
    '#default_value' => variable_get('uc_authnet_api_live_gateway_url', UC_AUTHORIZENET_LIVE_GATEWAY_URL),
  );

  $form['aim_settings'] = array(
    '#type' => 'fieldset',
    '#title' => t('AIM settings'),
    '#description' => t('These settings pertain to the Authorize.Net AIM payment method for card not present transactions.'),
  );
  $form['aim_settings']['uc_authnet_aim_txn_mode'] = array(
    '#type' => 'radios',
    '#title' => t('Transaction mode'),
    '#description' => t('Only specify a developer test account if you login to your account through https://test.authorize.net.<br />Adjust to live transactions when you are ready to start processing real payments.'),
    '#options' => array(
      'live' => t('Live transactions in a live account'),
      'live_test' => t('Test transactions in a live account'),
      'developer_test' => t('Developer test account transactions'),
    ),
    '#default_value' => variable_get('uc_authnet_aim_txn_mode', 'live_test'),
  );

  $form['aim_settings']['uc_authnet_aim_email_customer'] = array(
    '#type' => 'checkbox',
    '#title' => t('Tell Authorize.net to e-mail the customer a receipt based on your account settings.'),
    '#default_value' => variable_get('uc_authnet_aim_email_customer', FALSE),
  );
  $form['aim_settings']['uc_authnet_response_debug'] = array(
    '#type' => 'checkbox',
    '#title' => t('Log full API response messages from Authorize.net for debugging.'),
    '#default_value' => variable_get('uc_authnet_response_debug', FALSE),
  );


  $form['arb_settings'] = array(
    '#type' => 'fieldset',
    '#title' => t('ARB settings'),
    '#description' => t('These settings pertain to the Authorize.Net Automated Recurring Billing service.')
  );
  $form['arb_settings']['uc_authnet_arb_mode'] = array(
    '#type' => 'radios',
    '#title' => t('Transaction mode'),
    '#description' => t('Only specify developer mode if you login to your account through https://test.authorize.net.<br />Adjust to production mode when you are ready to start processing real recurring fees.'),
    '#options' => array(
      'production' => t('Production'),
      'developer' => t('Developer test'),
      'disabled' => t('Disabled'),
    ),
    '#default_value' => variable_get('uc_authnet_arb_mode', 'disabled'),
  );
  $form['arb_settings']['uc_authnet_md5_hash'] = array(
    '#type' => 'textfield',
    '#title' => t('MD5 Hash'),
    '#description' => t('<b>Note:</b> You must first configure credit card encryption before setting this.<br />Enter the value here you entered in your Auth.Net account settings.'),
    '#default_value' => $login_data['md5_hash'],
    '#access' => user_access('administer credit cards'),
  );
  $form['arb_settings']['uc_authnet_report_arb_post'] = array(
    '#type' => 'checkbox',
    '#title' => t('Log reported ARB payments in watchdog.'),
    '#description' => t('Make sure you have set your Silent POST URL in Authorize.Net to @url.', array('@url' => url('authnet/silent-post', array('absolute' => TRUE)))),
    '#default_value' => variable_get('uc_authnet_report_arb_post', FALSE),
  );

  $form['cim_settings'] = array(
    '#type' => 'fieldset',
    '#title' => t('CIM settings'),
    '#description' => t('These settings pertain to the Authorize.Net Customer Information Management service.')
  );
  $form['cim_settings']['uc_authnet_cim_profile'] = array(
    '#type' => 'checkbox',
    '#title' => t('Always create a CIM profile for securely storing CC info for later use.'),
    '#default_value' => variable_get('uc_authnet_cim_profile', FALSE),
  );
  $form['cim_settings']['uc_authnet_cim_mode'] = array(
    '#type' => 'radios',
    '#title' => t('Transaction mode'),
    '#description' => t('Only specify a developer test account if you login to your account through https://test.authorize.net.<br />Adjust to live transactions when you are ready to start processing real payments.'),
    '#options' => array(
      'production' => t('Production'),
      'developer' => t('Developer test'),
      'disabled' => t('Disabled'),
    ),
    '#default_value' => variable_get('uc_authnet_cim_mode', 'disabled'),
  );


  return $form;
}

/**
 * Implements hook_form_FORM_ID_alter() for uc_payment_method_settings_form().
 */
function uc_authorizenet_form_uc_payment_method_settings_form_alter(&$form, &$form_state) {
  if ($form_state['build_info']['args'][0] == 'credit') {
    $form['#submit'][] = 'uc_authorizenet_payment_gateway_settings_submit';
  }
}

/**
 * Submit handler for payment gateway settings form to encrypt fields.
 */
function uc_authorizenet_payment_gateway_settings_submit($form, &$form_state) {
  // If CC encryption has been configured properly.
  if ($key = uc_credit_encryption_key()) {
    // Setup our encryption object.
    $crypt = new UbercartEncryption();

    // Encrypt the Login ID, Transaction key, and MD5 Hash.
    if (!empty($form_state['values']['uc_authnet_md5_hash'])) {
      variable_set('uc_authnet_md5_hash', $crypt->encrypt($key, $form_state['values']['uc_authnet_md5_hash']));
    }

    // Store any errors.
    uc_store_encryption_errors($crypt, 'uc_authorizenet');
  }
}

/**
 * Main handler for processing credit card transactions.
 */
function uc_authorizenet_charge($order_id, $amount, $data) {
  // Load the order.
  $order = uc_order_load($order_id);

  // Perform the appropriate action based on the transaction type.
  switch ($data['txn_type']) {
    // Reference transactions are handled through Authorize.Net's CIM.
    case UC_CREDIT_REFERENCE_TXN:
      return _uc_authorizenet_cim_profile_charge($order, $amount, $data);

    // Set a reference only.
    case UC_CREDIT_REFERENCE_SET:
      // Return the error message if this failed.
      if ($message = _uc_authorizenet_cim_profile_create($order)) {
        return array('success' => FALSE, 'message' => $message);
      }
      else {
        return array('success' => TRUE, 'message' => t('New customer profile created successfully at Authorize.Net.'));
      }

    // Accommodate all other transaction types.
    default:
      return _uc_authorizenet_charge($order, $amount, $data);
  }
}

/**
 * Creates a CIM profile using an order's data.
 */
function _uc_authorizenet_cim_profile_create($order) {
  $server = variable_get('uc_authnet_cim_mode', 'disabled');

  // Help build the request.
  $request = _uc_authorizenet_cim_profile_create_request($order);

  // Request a profile from auth.net.
  $xml = _uc_authorizenet_xml_api_wrapper('createCustomerProfileRequest', _uc_authorizenet_array_to_xml($request));

  // Parse the response.
  $response = _uc_authorizenet_cim_parse_response(uc_authorizenet_xml_api($server, $xml));
  if ($response['resultCode'] == 'Error') {
    uc_order_comment_save($order->order_id, 0, t('Authorize.Net: Creating CIM profile failed.<br />@error - @text', array('@error' => $response['code'], '@text' => $response['text'])), 'admin');
    return $response['text'];
  }
  else {
    uc_order_comment_save($order->order_id, 0, t('Authorize.Net: CIM profile created - @id', array('@id' => $response['customerProfileId'])));
  }

  // Save the new credit reference to the db.
  $order->data = uc_credit_log_reference($order->order_id, $response['customerProfileId'], $order->payment_details['cc_number']);

  return '';
}

/**
 * Helper to create the CIM profile creation request.
 */
function _uc_authorizenet_cim_profile_create_request($order) {
  return array(
    'refId' => substr($order->order_id . '-' . REQUEST_TIME, 0, 20),
    'profile' => array(
      'merchantCustomerId' => substr($order->uid, 0, 20),
      'description' => substr(t('Order @order taking place at @date', array('@order' => $order->order_id, '@date' => format_date(REQUEST_TIME))), 0, 255),
      'email' => substr($order->primary_email, 0, 255),
      'paymentProfiles' => array(
        'billTo' => _uc_authorize_cim_xml_billto($order),
        'payment' => array(
          'creditCard' => array(
            'cardNumber' => $order->payment_details['cc_number'],
            'expirationDate' => $order->payment_details['cc_exp_year'] . '-' . str_pad($order->payment_details['cc_exp_month'], 2, '0', STR_PAD_LEFT),
          ),
        ),
      ),
      'shipToList' => _uc_authorize_cim_xml_shipto($order),
    ),
  );
}

/**
 * Uses a reference to charge to a CIM profile.
 */
function _uc_authorizenet_cim_profile_charge($order, $amount, $data) {
  global $user;
  $server = variable_get('uc_authnet_cim_mode', 'disabled');

  // Help build the request.
  $request = _uc_authorizenet_cim_profile_charge_request($order, $amount, $data);

  // Check error state.
  if (array_key_exists('errorCode', $request)) {
    $comment[] = $request['text'];
    $result = array(
      'success' => FALSE,
    );
  }

  // Request went off smooth.
  else {

    // Request a profile from auth.net.
    $xml = _uc_authorizenet_xml_api_wrapper('createCustomerProfileTransactionRequest', _uc_authorizenet_array_to_xml($request));

    // Parse the response.
    $response = _uc_authorizenet_cim_parse_response(uc_authorizenet_xml_api($server, $xml));

    // Error state.
    if ($response['resultCode'] == 'Error') {
      $result = array(
        'success' => FALSE,
      );
      $comment[] = '(' . $response['resultCode'] . ': ' . $response['text'] . ')';
    }

    // Transaction succeeded.
    else {
      $result = array(
        'success' => TRUE,
      );

      // Build info message.
      $types = uc_credit_transaction_types();
      $comment[] = t('<b>@type:</b> @amount', array('@type' => $types[$data['txn_type']], '@amount' => uc_currency_format($amount)));

      // Save a comment to the order.
      uc_order_comment_save($order->order_id, $user->uid, implode('<br />', $comment), 'admin');
    }
  }

  // Build the response to the payment gateway API.
  return $result + array(
    'comment' => implode(', ', $comment),
    'message' => implode('<br />', $comment),
    'uid' => $user->uid,
  );
}

/**
 * Helper for building the request for a CIM profile charge.
 */
function _uc_authorizenet_cim_profile_charge_request($order, $amount, $data) {
  $profile = _uc_authorizenet_cim_profile_get($order, $data['ref_id']);
  if ($profile['resultCode'] == 'Error') {
    return $profile;
  }
  else {
    return array(
      'refId' => substr($order->order_id . '-' . REQUEST_TIME, 0, 20),
      'transaction' => array(
        'profileTransAuthCapture' => array(
          'amount' => $amount,
          'customerProfileId' => $profile['customerProfileId'],
          'customerPaymentProfileId' => $profile['customerPaymentProfileId'],
          'order' => array(
            'invoiceNumber' => $order->order_id,
          ),
        ),
      ),
    );
  }
}

/**
 * Gets a CIM profile stored at Authorize.Net.
 */
function _uc_authorizenet_cim_profile_get($order, $profile_id) {
  $server = variable_get('uc_authnet_cim_mode', 'disabled');
  $request = array(
    'customerProfileId' => $profile_id,
  );

  // Request a profile from auth.net.
  $xml = _uc_authorizenet_xml_api_wrapper('getCustomerProfileRequest', _uc_authorizenet_array_to_xml($request));

  // Parse the response.
  $response = _uc_authorizenet_cim_parse_response(uc_authorizenet_xml_api($server, $xml));

  return $response;
}

/**
 * Gets a CIM payment profile stored at auth.net.
 */
function _uc_authorizenet_cim_payment_profile_get($order, $profile_id, $payment_profile_id) {
  $server = variable_get('uc_authnet_cim_mode', 'disabled');
  $request = array(
    'customerProfileId' => $profile_id,
  );

  // Request a profile from auth.net.
  $xml = _uc_authorizenet_xml_api_wrapper('getCustomerPaymentProfileRequest', _uc_authorizenet_array_to_xml($request));
  // Parse the response.
  $response = _uc_authorizenet_cim_parse_response(uc_authorizenet_xml_api($server, $xml));
  return $response['resultCode'] == 'Error' ? FALSE : $response;
}

/**
 * Handles authorizations and captures through AIM at Authorize.Net
 */
function _uc_authorizenet_charge($order, $amount, $data) {
  global $user;

  // Build a description of the order for logging in Auth.Net.
  $description = array();
  foreach ((array) $order->products as $product) {
    $description[] = $product->qty . 'x ' . $product->model;
  }

  $billing_country = uc_get_country_data(array('country_id' => $order->billing_country));
  $delivery_country = uc_get_country_data(array('country_id' => $order->delivery_country));

  // Build the POST data for the transaction.
  $submit_data = array(
    // Merchant information.
    'x_login' => trim(variable_get('uc_authnet_api_login_id', '')),
    'x_tran_key' => trim(variable_get('uc_authnet_api_transaction_key', '')),

    // Transaction information.
    'x_version' => '3.1',
    'x_type' => _uc_authorizenet_txn_map($data['txn_type']),
    // 'x_method' => $order->payment_method == 'credit' ? 'CC' : 'ECHECK',
    'x_method' => 'CC',
    // 'x_recurring_billing' => 'FALSE',
    'x_amount' => uc_currency_format($amount, FALSE, FALSE, '.'),
    'x_card_num' => $order->payment_details['cc_number'],
    'x_exp_date' => $order->payment_details['cc_exp_month'] . '/' . $order->payment_details['cc_exp_year'],
    'x_card_code' => $order->payment_details['cc_cvv'],
    // 'x_trans_id' => '',
    // 'x_auth_code' => '',
    'x_test_request' => variable_get('uc_authnet_aim_txn_mode', 'live_test') == 'live_test' ? 'TRUE' : 'FALSE',
    'x_duplicate_window' => variable_get('uc_authnet_duplicate_window', 120),

    // Order information.
    'x_invoice_num' => $order->order_id,
    'x_description' => substr(implode(', ', $description), 0, 255),

    // Customer information.
    'x_first_name' => substr($order->billing_first_name, 0, 50),
    'x_last_name' => substr($order->billing_last_name, 0, 50),
    'x_company' => substr($order->billing_company, 0, 50),
    'x_address' => substr($order->billing_street1, 0, 60),
    'x_city' => substr($order->billing_city, 0, 40),
    'x_state' => substr(uc_get_zone_code($order->billing_zone), 0, 40),
    'x_zip' => substr($order->billing_postal_code, 0, 20),
    'x_country' => !$billing_country ? '' : $billing_country[0]['country_iso_code_2'],
    'x_phone' => substr($order->billing_phone, 0, 25),
    // 'x_fax' => substr('', 0, 25),
    'x_email' => substr($order->primary_email, 0, 255),
    'x_cust_id' => substr($order->uid, 0, 20),
    'x_customer_ip' => substr(ip_address(), 0, 15),

    // Shipping information.
    'x_ship_to_first_name' => substr($order->delivery_first_name, 0, 50),
    'x_ship_to_last_name' => substr($order->delivery_last_name, 0, 50),
    'x_ship_to_company' => substr($order->delivery_company, 0, 50),
    'x_ship_to_address' => substr($order->delivery_street1, 0, 60),
    'x_ship_to_city' => substr($order->delivery_city, 0, 40),
    'x_ship_to_state' => substr(uc_get_zone_code($order->delivery_zone), 0, 40),
    'x_ship_to_zip' => substr($order->delivery_postal_code, 0, 20),
    'x_ship_to_country' => !$delivery_country ? '' : $delivery_country[0]['country_iso_code_2'],

    // Extra information.
    'x_delim_data' => 'TRUE',
    'x_delim_char' => '|',
    'x_encap_char' => '"',
    'x_relay_response' => 'FALSE',
    'x_email_customer' => variable_get('uc_authnet_aim_email_customer', FALSE) ? 'TRUE' : 'FALSE',
  );

  if ($data['txn_type'] == UC_CREDIT_PRIOR_AUTH_CAPTURE) {
    $submit_data['x_trans_id'] = $data['auth_id'];
  }

  // Determine the correct URL based on the transaction mode.
  if (variable_get('uc_authnet_aim_txn_mode', 'live_test') == 'developer_test') {
    $post_url = variable_get('uc_authnet_api_test_gateway_url', UC_AUTHORIZENET_TEST_GATEWAY_URL);
  }
  else {
    $post_url = variable_get('uc_authnet_api_live_gateway_url', UC_AUTHORIZENET_LIVE_GATEWAY_URL);
  }

  // Translate the data array into a string we can POST.
  $post_fields = array();
  foreach ($submit_data as $key => $value) {
    $post_fields[] = $key . '=' . urlencode($value);
  }

  // Setup the cURL request.
  $ch = curl_init();
  curl_setopt($ch, CURLOPT_URL, $post_url);
  curl_setopt($ch, CURLOPT_VERBOSE, 0);
  curl_setopt($ch, CURLOPT_POST, 1);
  curl_setopt($ch, CURLOPT_POSTFIELDS, implode('&', $post_fields));
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
  curl_setopt($ch, CURLOPT_NOPROGRESS, 1);
  curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
  $result = curl_exec($ch);

  // Log any errors to the watchdog.
  if ($error = curl_error($ch)) {
    watchdog('uc_authorizenet', 'cURL error: @error', array('@error' => $error), WATCHDOG_ERROR);
    return array('success' => FALSE);
  }
  curl_close($ch);

  $response = explode('|', $result);

  if (variable_get('uc_authnet_response_debug', FALSE)) {
    watchdog('uc_authorizenet', 'Debug response: !data', array('!data' => '<pre>' . check_plain(print_r($response, TRUE)) . '</pre>'));
  }

  // Trim off the encapsulating character from the results.
  for ($i = 0; $i < count($response); $i++) {
    $response[$i] = substr($response[$i], 1, strlen($response[$i]) - 2);
  }

  /**
   * Response key index:
   * 0 = Response Code
   * 2 = Response Reason Code
   * 3 = Response Reason Text
   * 4 = Authorization Code
   * 5 = Address Verification Service (AVS) Response
   * 6 = Transaction ID; needed for CREDIT, PRIOR_AUTH_CAPTURE, and VOID transactions.
   * 9 = Amount
   * 11 = Transaction Type
   * 32 = Tax Amount Charged
   * 37 = Transaction Response MD5 Hash
   * 38 = Card Code (CVV) Response
   */

  // If we didn't get an approval response code...
  if ($response[0] != '1') {
    // Fail the charge with the reason text in the decline message.
    $result = array(
      'success' => FALSE,
      'message' => t('Credit card payment declined: @message', array('@message' => $response[3])),
      'uid' => $user->uid,
    );
  }
  else {
    // Build a message for display and comments in the payments table.
    $message = t('Type: @type<br />ID: @id', array('@type' => _uc_authorizenet_txn_type($response[11]), '@id' => $response[6]));
    $result = array(
      'success' => TRUE,
      'comment' => $message,
      'message' => $message,
      'data' => array('module' => 'uc_authorizenet', 'txn_type' => $response[11], 'txn_id' => $response[6], 'txn_authcode' => $response[4]),
      'uid' => $user->uid,
    );

    // If this was an authorization only transaction...
    if ($data['txn_type'] == UC_CREDIT_AUTH_ONLY) {
      // Log the authorization to the order.
      uc_credit_log_authorization($order->order_id, $response[6], $amount);
    }
    elseif ($data['txn_type'] == UC_CREDIT_PRIOR_AUTH_CAPTURE) {
      uc_credit_log_prior_auth_capture($order->order_id, $data['auth_id']);
    }

    // Create a transaction reference if specified in the payment gateway
    // settings and this is an appropriate transaction type.
    if (variable_get('uc_authnet_cim_profile', FALSE) && in_array($data['txn_type'], array(UC_CREDIT_AUTH_ONLY, UC_CREDIT_AUTH_CAPTURE))) {
      // Ignore the returned message for now; that will appear in the comments.
      _uc_authorizenet_cim_profile_create($order);
    }
  }

  // Don't log this as a payment money wasn't actually captured.
  if (in_array($data['txn_type'], array(UC_CREDIT_AUTH_ONLY))) {
    $result['log_payment'] = FALSE;
  }

  // Build an admin order comment.
  $comment = t('<b>@type</b><br /><b>@status:</b> @message<br />Amount: @amount<br />AVS response: @avs',
    array('@type' => _uc_authorizenet_txn_type($response[11]), '@status' => $result['success'] ? t('ACCEPTED') : t('REJECTED'), '@message' => $response[3], '@amount' => uc_currency_format($response[9]), '@avs' => _uc_authorizenet_avs($response[5])));

  // Add the CVV response if enabled.
  if (variable_get('uc_credit_cvv_enabled', TRUE)) {
    $comment .= '<br />' . t('CVV match: @cvv', array('@cvv' => _uc_authorizenet_cvv($response[38])));
  }

  // Save the comment to the order.
  uc_order_comment_save($order->order_id, $user->uid, $comment, 'admin');

  return $result;
}

/**
 * Implements hook_recurring_fee().
 */
function uc_authorizenet_recurring_fee($order, $fee) {
  // Don't process the fee if ARB is disabled in the gateway settings.
  if (variable_get('uc_authnet_arb_mode', 'disabled') == 'disabled') {
    return FALSE;
  }

  return uc_authorizenet_arb_create($order, $fee);
}

/**
 * Implements hook_recurring_fee_ops().
 */
function uc_authorizenet_recurring_fee_ops($context, $fee) {
  $ops = array();

  switch ($context) {
    case 'fee_admin':
      if ($fee['remaining_intervals'] > 0) {
        $ops[] = l(t('update'), 'admin/store/orders/recurring/' . $fee['rfid'] . '/arb-update');
        $ops[] = l(t('cancel'), 'admin/store/orders/recurring/' . $fee['rfid'] . '/arb-cancel');
      }
      else {
        $ops[] = l(t('delete'), 'admin/store/orders/recurring/' . $fee['rfid'] . '/delete');
      }
      break;

    case 'user':
      $ops[] = l(t('update'), 'user/' . $fee['uid'] . '/recurring/' . $fee['rfid'] . '/arb-update');
      $ops[] = l(t('cancel'), 'user/' . $fee['uid'] . '/recurring/' . $fee['rfid'] . '/arb-cancel');
      break;
  }


  return $ops;
}

/**
 * Sends an XML API Request to Authorize.Net.
 *
 * @param $server
 *   The name of the server to send a request to - 'production' or 'developer'.
 * @param $xml
 *   The XML to send to Authorize.Net.
 * @param $callback
 *   The name of the function that should process the response.
 *
 * @return
 *   TRUE or FALSE indicating the success of the API request.
 */
function uc_authorizenet_xml_api($server, $xml) {
  if ($server == 'production') {
    $post_url = 'https://api.authorize.net/xml/v1/request.api';
  }
  elseif ($server == 'developer') {
    $post_url = 'https://apitest.authorize.net/xml/v1/request.api';
  }
  else {
    return FALSE;
  }

  $ch = curl_init();
  curl_setopt($ch, CURLOPT_URL, $post_url);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  curl_setopt($ch, CURLOPT_HTTPHEADER, Array("Content-Type: text/xml"));
  curl_setopt($ch, CURLOPT_HEADER, 1);
  curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
  curl_setopt($ch, CURLOPT_POST, 1);
  curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
  $response = curl_exec($ch);

  // Log any errors to the watchdog.
  if ($error = curl_error($ch)) {
    watchdog('uc_authorizenet', 'cURL error: @error', array('@error' => $error), WATCHDOG_ERROR);
    return FALSE;
  }
  curl_close($ch);

  return $response;
}

/**
 * Sends an ARB Create request via the XML API.
 *
 * @param $order
 *   The order object containing billing and shipping information.
 * @param $fee
 *   An array of data describing the recurring fee.
 *
 * @return
 *   TRUE or FALSE indicating the success of the request.
 */
function uc_authorizenet_arb_create($order, $fee) {
  $server = variable_get('uc_authnet_arb_mode', 'disabled');

  // Setup variables for the payment schedule.
  list($length, $unit) = explode(' ', $fee->regular_interval);
  list($trial_length, $trial_unit) = explode(' ', $fee->initial_charge);

  // Convert weeks and years to days.
  if ($unit == 'weeks') {
    $length *= 7;
    $unit = 'days';
  }
  elseif ($unit == 'years') {
    $length *= 365;
    $unit = 'days';
  }

  // Get a default SKU if none was supplied.
  if (empty($fee->model)) {
    $fee->model = db_query("SELECT model FROM {uc_products} WHERE nid = :nid", array(':nid' => $fee->nid))->fetchField();
  }

  // Make sure we have valid values for Authorize.Net.
  if ($length <= 0 || $unit == 'days' && $length > 365 || $unit == 'months' && $length > 12) {
    watchdog('uc_authorizenet', 'Product @sku has invalid interval settings for Authorize.Net - @length @unit', array('@sku' => $fee->model, '@length' => $length, '@unit' => $unit), WATCHDOG_ERROR);
    return FALSE;
  }

  // Get the country data for the billing and shipping information.
  $billing_country = uc_get_country_data(array('country_id' => $order->billing_country));
  $delivery_country = uc_get_country_data(array('country_id' => $order->delivery_country));

  // Build the data array for the request.
  $data = array(
    'refId' => substr($order->order_id . '-' . REQUEST_TIME, 0, 20),
    'subscription' => array(
      'name' => substr(t('Order @order_id', array('@order_id' => $order->order_id)), 0, 50),
      'paymentSchedule' => array(
        'interval' => array(
          'length' => $length,
          'unit' => $unit,
        ),
        'startDate' => date('Y-m-d', strtotime('+ ' . $fee->initial_charge)),
        'totalOccurrences' => $fee->number_intervals,
        'trialOccurrences' => '0',
      ),
      'amount' => round($fee->fee_amount, 2),
      'trialAmount' => 0,
      'payment' => array(), // Data inserted below based on payment method.
      'order' => array(
        'invoiceNumber' => substr($order->order_id, 0, 20),
        'description' => substr(t('Order @order_id - @sku', array('@order_id' => $order->order_id, '@sku' => $fee->model)), 0, 255),
      ),
      'customer' => array(
        'id' => substr($order->uid, 0, 20),
        'email' => substr($order->primary_email, 0, 255),
        'phoneNumber' => substr($order->billing_phone, 0, 25),
        // 'faxNumber' => '',
      ),
      'billTo' => array(
        'firstName' => substr($order->billing_first_name, 0, 50),
        'lastName' => substr($order->billing_last_name, 0, 50),
        'company' => substr($order->billing_company, 0, 50),
        'address' => substr($order->billing_street1, 0, 60),
        'city' => substr($order->billing_city, 0, 40),
        'state' => substr(uc_get_zone_code($order->billing_zone), 0, 2),
        'zip' => substr($order->billing_postal_code, 0, 20),
        'country' => !$billing_country ? '' : $billing_country[0]['country_iso_code_2'],
      ),
      'shipTo' => array(
        'firstName' => substr($order->delivery_first_name, 0, 50),
        'lastName' => substr($order->delivery_last_name, 0, 50),
        'company' => substr($order->delivery_company, 0, 50),
        'address' => substr($order->delivery_street1, 0, 60),
        'city' => substr($order->delivery_city, 0, 40),
        'state' => substr(uc_get_zone_code($order->delivery_zone), 0, 2),
        'zip' => substr($order->delivery_postal_code, 0, 20),
        'country' => !$delivery_country ? '' : $delivery_country[0]['country_iso_code_2'],
      ),
    ),
  );

  // Strip out the shipping info if it isn't necessary.
  if (empty($data['subscription']['shipTo']['firstName'])) {
    unset($data['subscription']['shipTo']);
  }

  // Add the payment information to the data array based on the payment method.
  if ($order->payment_method == 'credit') {
    if ($order->payment_details['cc_exp_month'] < 10) {
      $order->payment_details['cc_exp_month'] = '0' . $order->payment_details['cc_exp_month'];
    }

    $data['subscription']['payment'] = array(
      'creditCard' => array(
        'cardNumber' => $order->payment_details['cc_number'],
        'expirationDate' => $order->payment_details['cc_exp_year'] . '-' . $order->payment_details['cc_exp_month'],
      ),
    );
  }

  // Build the XML string.
  $xml = _uc_authorizenet_xml_api_wrapper('ARBCreateSubscriptionRequest', _uc_authorizenet_array_to_xml($data));

  // Send the request off to the server and get the response.
  $response = uc_authorizenet_xml_api($server, $xml);

  // Fail if the response is empty or FALSE.
  if (!$response) {
    return FALSE;
  }

  // Parse the response into a data array.
  $data = _uc_authorizenet_arb_parse_response($response);

  if ($data['resultCode'] == 'Error') {
    uc_order_comment_save($order->order_id, 0, t('Authorize.Net: Recurring fee for @model failed.<br />@error - @text', array('@model' => $fee->model, '@error' => $data['code'], '@text' => $data['text'])), 'admin');
    return FALSE;
  }

  $user_fee = array(
    'rfid' => 0,
    'uid' => $order->uid,
    'fee_handler' => 'uc_authorizenet',
    'next_charge' => strtotime('+' . $fee->initial_charge),
    'fee_amount' => $fee->fee_amount,
    'regular_interval' => $fee->regular_interval,
    'remaining_intervals' => $fee->number_intervals,
    'charged_intervals' => 0,
    'order_id' => $order->order_id,
    'data' => $data['subscriptionId'],
  );

  uc_recurring_fee_save('user', $user_fee);

  uc_order_comment_save($order->order_id, 0, t('Authorize.Net: Recurring fee setup for @model.<br />Subscription ID: @subscription_id', array('@model' => $fee->model, '@subscription_id' => $data['subscriptionId'])), 'admin');

  return TRUE;
}

/**
 * Updates an ARB subscription; for simplicity's sake, payment schedule
 * information cannot be updated at this time.
 *
 * @param $subscription_id
 *   The ID of the subscription at Authorize.Net.
 * @param $updates
 *   An array of data to update using key/value pairs from the XML API for ARB;
 *   keys should be children of the subscription element in the XML.
 *   See the ARB_guide.pdf from Authorize.Net for ARBCreateSubscriptionRequests.
 *
 * @return
 *   TRUE or FALSE indicating the success of the cancellation.
 */
function uc_authorizenet_arb_update($subscription_id, $updates, $order_id = NULL) {
  $server = variable_get('uc_authnet_arb_mode', 'disabled');

  unset($updates['paymentSchedule']);

  // Build the data array for the request.
  $data = array(
    'refId' => substr($order->order_id . '-' . REQUEST_TIME, 0, 20),
    'subscriptionId' => $subscription_id,
    'subscription' => $updates
  );

  // Build the XML string.
  $xml = _uc_authorizenet_xml_api_wrapper('ARBUpdateSubscriptionRequest', _uc_authorizenet_array_to_xml($data));

  // Send the request off to the server and get the response.
  $response = uc_authorizenet_xml_api($server, $xml);

  // Fail if the response is empty or FALSE.
  if (!$response) {
    return FALSE;
  }

  // Parse the response into a data array.
  $data = _uc_authorizenet_arb_parse_response($response);

  if ($data['resultCode'] == 'Error') {
    if (!empty($order_id)) {
      uc_order_comment_save($order_id, 0, t('Authorize.Net: Subscription @subscription_id updated failed.<br />@error - @text', array('@subscription_id' => $subscription_id, '@error' => $data['code'], '@text' => $data['text'])), 'admin');
    }
    return FALSE;
  }

  uc_order_comment_save($order_id, 0, t('Authorize.Net: Subscription @subscription_id updated.', array('@subscription_id' => $subscription_id)), 'admin');

  return TRUE;
}

/**
 * Cancels an ARB subscription.
 *
 * @param $subscription
 *   The ID of the subscription at Authorize.Net.
 * @param $order_id
 *   Optional. The ID of the order the recurring fee was attached to.
 * @param $fee
 *   Optional. The data array for the recurring fee being canceled.
 *
 * @return
 *   TRUE or FALSE indicating the success of the cancellation.
 */
function uc_authorizenet_arb_cancel($subscription_id, $order_id = NULL, $fee = array()) {
  $server = variable_get('uc_authnet_arb_mode', 'disabled');

  // Build the data array for the request.
  $data = array(
    'refId' => substr($order->order_id . '-' . REQUEST_TIME, 0, 20),
    'subscriptionId' => $subscription_id,
  );

  // Build the XML string.
  $xml = _uc_authorizenet_xml_api_wrapper('ARBCancelSubscriptionRequest', _uc_authorizenet_array_to_xml($data));

  // Send the request off to the server and get the response.
  $response = uc_authorizenet_xml_api($server, $xml);

  // Fail if the response is empty or FALSE.
  if (!$response) {
    return FALSE;
  }

  // Parse the response into a data array.
  $data = _uc_authorizenet_arb_parse_response($response);

  if ($data['resultCode'] == 'Error') {
    if (!empty($order_id)) {
      uc_order_comment_save($order_id, 0, t('Authorize.Net: Subscription @subscription_id cancellation failed.<br />@error - @text', array('@subscription_id' => $subscription_id, '@error' => $data['code'], '@text' => $data['text'])), 'admin');
    }
    return FALSE;
  }

  uc_order_comment_save($order_id, 0, t('Authorize.Net: Subscription @subscription_id canceled.', array('@subscription_id' => $subscription_id)), 'admin');

  // Let other modules act on the canceled fee.
  if (!empty($fee)) {
    module_invoke_all('uc_arb_cancel', $fee);
  }

  return TRUE;
}

/**
 * Wraps XML API request child elements in the request element and includes
 * the merchant authentication information.
 */
function _uc_authorizenet_xml_api_wrapper($request, $xml) {
  return '<?xml version="1.0" encoding="utf-8"?><' . $request
        . ' xmlns="AnetApi/xml/v1/schema/AnetApiSchema.xsd"><merchantAuthentication>'
        . '<name>' . trim(variable_get('uc_authnet_api_login_id', '')) . '</name>'
        . '<transactionKey>' . trim(variable_get('uc_authnet_api_transaction_key', ''))
        . '</transactionKey></merchantAuthentication>' . $xml . '</' . $request . '>';
}

/**
 * Converts a hierarchical array of elements into an XML string.
 */
function _uc_authorizenet_array_to_xml($data) {
  $xml = '';

  // Loop through the elements in the data array.
  foreach ($data as $element => $contents) {
    if (is_array($contents)) {
      // Render the element with its child elements.
      $xml .= '<' . $element . '>' . _uc_authorizenet_array_to_xml($contents) . '</' . $element . '>';
    }
    else {
      // Render the element with its contents.
      $xml .= '<' . $element . '>' . htmlspecialchars($contents) . '</' . $element . '>';
    }
  }

  return $xml;
}

/**
 * Returns the message text for an AVS response code.
 */
function _uc_authorizenet_avs($code) {
  $text = $code . ' - ';

  switch ($code) {
    case 'A':
      $text .= t('Address (Street) matches, ZIP does not');
      break;
    case 'B':
      $text .= t('Address information not provided for AVS check');
      break;
    case 'E':
      $text .= t('AVS error');
      break;
    case 'G':
      $text .= t('Non-U.S. Card Issuing Bank');
      break;
    case 'N':
      $text .= t('No Match on Address (Street) or ZIP');
      break;
    case 'P':
      $text .= t('AVS not applicable for this transaction');
      break;
    case 'R':
      $text .= t('Retry – System unavailable or timed out');
      break;
    case 'S':
      $text .= t('Service not supported by issuer');
      break;
    case 'U':
      $text .= t('Address information is unavailable');
      break;
    case 'W':
      $text .= t('Nine digit ZIP matches, Address (Street) does not');
      break;
    case 'X':
      $text .= t('Address (Street) and nine digit ZIP match');
      break;
    case 'Y':
      $text .= t('Address (Street) and five digit ZIP match');
      break;
    case 'Z':
      $text .= t('Five digit ZIP matches, Address (Street) does not');
      break;
  }

  return $text;
}

/**
 * Returns the message text for a CVV match.
 */
function _uc_authorizenet_cvv($code) {
  $text = $code . ' - ';

  switch ($code) {
    case 'M':
      $text .= t('Match');
      break;
    case 'N':
      $text .= t('No Match');
      break;
    case 'P':
      $text .= t('Not Processed');
      break;
    case 'S':
      $text .= t('Should have been present');
      break;
    case 'U':
      $text .= t('Issuer unable to process request');
      break;
  }

  return $text;
}

/**
 * Returns the title of the transaction type.
 */
function _uc_authorizenet_txn_type($type) {
  switch (strtoupper($type)) {
    case 'AUTH_CAPTURE':
      return t('Authorization and capture');
    case 'AUTH_ONLY':
      return t('Authorization only');
    case 'PRIOR_AUTH_CAPTURE':
      return t('Prior authorization capture');
    case 'CAPTURE_ONLY':
      return t('Capture only');
    case 'CREDIT':
      return t('Credit');
    case 'VOID':
      return t('Void');
  }
}

/**
 * Returns the Auth.Net transaction type corresponding to a UC type.
 */
function _uc_authorizenet_txn_map($type) {
  switch ($type) {
    case UC_CREDIT_AUTH_ONLY:
      return 'AUTH_ONLY';
    case UC_CREDIT_PRIOR_AUTH_CAPTURE:
      return 'PRIOR_AUTH_CAPTURE';
    case UC_CREDIT_AUTH_CAPTURE:
      return 'AUTH_CAPTURE';
    case UC_CREDIT_CREDIT:
      return 'CREDIT';
    case UC_CREDIT_VOID:
      return 'VOID';
  }
}

/**
 * Maps an order's billing information to an array for later XML conversion.
 */
function _uc_authorize_cim_xml_billto($order) {
  $billing_country = uc_get_country_data(array('country_id' => $order->billing_country));
  return array(
    'firstName' => substr($order->billing_first_name, 0, 50),
    'lastName' => substr($order->billing_last_name, 0, 50),
    'company' => substr($order->billing_company, 0, 50),
    'address' => substr($order->billing_street1, 0, 60),
    'city' => substr($order->billing_city, 0, 40),
    'state' => substr(uc_get_zone_code($order->billing_zone), 0, 2),
    'zip' => substr($order->billing_postal_code, 0, 20),
    'country' => !$billing_country ? '' : $billing_country[0]['country_iso_code_2'],
    'phoneNumber' => substr($order->billing_phone, 0, 25),
  );
}

/**
 * Maps an order's shipping information to an array for later XML conversion.
 */
function _uc_authorize_cim_xml_shipto($order) {
  $delivery_country = uc_get_country_data(array('country_id' => $order->delivery_country));
  return array(
    'firstName' => substr($order->delivery_first_name, 0, 50),
    'lastName' => substr($order->delivery_last_name, 0, 50),
    'company' => substr($order->delivery_company, 0, 50),
    'address' => substr($order->delivery_street1, 0, 60),
    'city' => substr($order->delivery_city, 0, 40),
    'state' => substr(uc_get_zone_code($order->delivery_zone), 0, 2),
    'zip' => substr($order->delivery_postal_code, 0, 20),
    'country' => !$delivery_country ? '' : $delivery_country[0]['country_iso_code_2'],
  );
}

/**
 * Parses an Authorize.Net XML CIM API response.
 */
function _uc_authorizenet_cim_parse_response($content) {
  // Find the elements in the XML and build the return array.
  $data = array(
    'refId' => _uc_authorizenet_substr_between($content, 'refId'),
    'resultCode' => _uc_authorizenet_substr_between($content, 'resultCode'),
    'code' => _uc_authorizenet_substr_between($content, 'code'),
    'text' => _uc_authorizenet_substr_between($content, 'text'),
    'customerProfileId' => _uc_authorizenet_substr_between($content, 'customerProfileId'),
    'directResponse' => _uc_authorizenet_substr_between($content, 'directResponse'),
    'customerPaymentProfileId' => _uc_authorizenet_substr_between($content, 'customerPaymentProfileId'),
    'customerAddressId' => _uc_authorizenet_substr_between($content, 'customerAddressId'),
  );

  return $data;
}

/**
 * Parses an Authorize.Net XML API response; from sample PHP for ARB.
 */
function _uc_authorizenet_arb_parse_response($content) {
  // Find the elements in the XML and build the return array.
  $data = array(
    'refId' => _uc_authorizenet_substr_between($content, 'refId'),
    'resultCode' => _uc_authorizenet_substr_between($content, 'resultCode'),
    'code' => _uc_authorizenet_substr_between($content, 'code'),
    'text' => _uc_authorizenet_substr_between($content, 'text'),
    'subscriptionId' => _uc_authorizenet_substr_between($content, 'subscriptionId'),
  );

  return $data;
}

/**
 * Helper function for parsing responses; adapted from sample PHP for ARB.
 */
function _uc_authorizenet_substr_between($string, $element) {
  $open = '<' . $element . '>';
  $close = '</' . $element . '>';

  // Fail if we can't find the open or close tag for the element.
  if (strpos($string, $open) === FALSE || strpos($string, $close) === FALSE) {
    return FALSE;
  }

  $start = strpos($string, $open) + strlen($open);
  $end = strpos($string, $close);

  return substr($string, $start, $end - $start);
}

/**
 * Decrypts the login data for using Auth.Net APIs.
 */
function _uc_authorizenet_login_data() {
  static $data;

  if (!empty($data)) {
    return $data;
  }

  $md5_hash = variable_get('uc_authnet_md5_hash', '');

  // If CC encryption has been configured properly.
  if ($key = uc_credit_encryption_key()) {
    // Setup our encryption object.
    $crypt = new UbercartEncryption();

    // Decrypt the MD5 Hash.
    if (!empty($md5_hash)) {
      $md5_hash = $crypt->decrypt($key, $md5_hash);
    }

    // Store any errors.
    uc_store_encryption_errors($crypt, 'uc_authorizenet');
  }

  $data = array(
    'md5_hash' => $md5_hash,
  );

  return $data;
}
