<?php
/**
 * Easily interact with the Authorize.Net AIM API.
 *
 * Example Authorize and Capture Transaction against the Sandbox:
 * <code>
 * <?php require_once 'AuthorizeNet.php'
 * $sale = new AuthorizeNetAIM;
 * $sale->setFields(
 *     array(
 *    'amount' => '4.99',
 *    'card_num' => '411111111111111',
 *    'exp_date' => '0515'
 *    )
 * );
 * $response = $sale->authorizeAndCapture();
 * if ($response->approved) {
 *     echo "Sale successful!"; } else {
 *     echo $response->error_message;
 * }
 * ?>
 * </code>
 *
 * Note: To send requests to the live gateway, either define this:
 * define("AUTHORIZENET_SANDBOX", false);
 *   -- OR --
 * $sale = new AuthorizeNetAIM;
 * $sale->setSandbox(false);
 *
 * @package    AuthorizeNet
 * @subpackage AuthorizeNetAIM
 * @link       http://www.authorize.net/support/AIM_guide.pdf AIM Guide
 */


/**
 * Builds and sends an AuthorizeNet AIM Request.
 *
 * @package    AuthorizeNet
 * @subpackage AuthorizeNetAIM
 */
class AuthorizeNetAIM extends AuthorizeNetRequest {


	const LIVE_URL    = 'https://secure2.authorize.net/gateway/transact.dll';
	const SANDBOX_URL = 'https://test.authorize.net/gateway/transact.dll';

	/**
	 * Holds all the x_* name/values that will be posted in the request.
	 * Default values are provided for best practice fields.
	 */
	protected $_x_post_fields = array(
		'version'        => '3.1',
		'delim_char'     => ',',
		'delim_data'     => 'TRUE',
		'relay_response' => 'FALSE',
		'encap_char'     => '|',
	);

	/**
	 * Only used if merchant wants to send multiple line items about the charge.
	 */
	private $_additional_line_items = array();

	/**
	 * Only used if merchant wants to send custom fields.
	 */
	private $_custom_fields = array();

	/**
	 * Checks to make sure a field is actually in the API before setting.
	 * Set to false to skip this check.
	 */
	public $verify_x_fields = true;

	/**
	 * A list of all fields in the AIM API.
	 * Used to warn user if they try to set a field not offered in the API.
	 */
	private $_all_aim_fields = array(
		'address',
		'allow_partial_auth',
		'amount',
		'auth_code',
		'authentication_indicator',
		'bank_aba_code',
		'bank_acct_name',
		'bank_acct_num',
		'bank_acct_type',
		'bank_check_number',
		'bank_name',
		'card_code',
		'card_num',
		'cardholder_authentication_value',
		'city',
		'company',
		'country',
		'cust_id',
		'customer_ip',
		'delim_char',
		'delim_data',
		'description',
		'duplicate_window',
		'duty',
		'echeck_type',
		'email',
		'email_customer',
		'encap_char',
		'exp_date',
		'fax',
		'first_name',
		'footer_email_receipt',
		'freight',
		'header_email_receipt',
		'invoice_num',
		'last_name',
		'line_item',
		'login',
		'method',
		'phone',
		'po_num',
		'recurring_billing',
		'relay_response',
		'ship_to_address',
		'ship_to_city',
		'ship_to_company',
		'ship_to_country',
		'ship_to_first_name',
		'ship_to_last_name',
		'ship_to_state',
		'ship_to_zip',
		'split_tender_id',
		'state',
		'tax',
		'tax_exempt',
		'test_request',
		'tran_key',
		'trans_id',
		'type',
		'version',
		'zip',
	);

	/**
	 * Do an AUTH_CAPTURE transaction.
	 *
	 * Required "x_" fields: card_num, exp_date, amount
	 *
	 * @param string $amount   The dollar amount to charge
	 * @param string $card_num The credit card number
	 * @param string $exp_date CC expiration date
	 *
	 * @return AuthorizeNetAIM_Response
	 */
	public function authorizeAndCapture( $amount = false, $card_num = false, $exp_date = false ) {
		( $amount ? $this->amount = $amount : null );
		( $card_num ? $this->card_num = $card_num : null );
		( $exp_date ? $this->exp_date = $exp_date : null );
		$this->type = 'AUTH_CAPTURE';
		return $this->_sendRequest();
	}

	/**
	 * Do a PRIOR_AUTH_CAPTURE transaction.
	 *
	 * Required "x_" field: trans_id(The transaction id of the prior auth, unless split
	 * tender, then set x_split_tender_id manually.)
	 * amount (only if lesser than original auth)
	 *
	 * @param string $trans_id Transaction id to charge
	 * @param string $amount   Dollar amount to charge if lesser than auth
	 *
	 * @return AuthorizeNetAIM_Response
	 */
	public function priorAuthCapture( $trans_id = false, $amount = false ) {
		( $trans_id ? $this->trans_id = $trans_id : null );
		( $amount ? $this->amount = $amount : null );
		$this->type = 'PRIOR_AUTH_CAPTURE';
		return $this->_sendRequest();
	}

	/**
	 * Do an AUTH_ONLY transaction.
	 *
	 * Required "x_" fields: card_num, exp_date, amount
	 *
	 * @param string $amount   The dollar amount to charge
	 * @param string $card_num The credit card number
	 * @param string $exp_date CC expiration date
	 *
	 * @return AuthorizeNetAIM_Response
	 */
	public function authorizeOnly( $amount = false, $card_num = false, $exp_date = false ) {
		( $amount ? $this->amount = $amount : null );
		( $card_num ? $this->card_num = $card_num : null );
		( $exp_date ? $this->exp_date = $exp_date : null );
		$this->type = 'AUTH_ONLY';
		return $this->_sendRequest();
	}

	/**
	 * Do a VOID transaction.
	 *
	 * Required "x_" field: trans_id(The transaction id of the prior auth, unless split
	 * tender, then set x_split_tender_id manually.)
	 *
	 * @param string $trans_id Transaction id to void
	 *
	 * @return AuthorizeNetAIM_Response
	 */
	public function void( $trans_id = false ) {
		( $trans_id ? $this->trans_id = $trans_id : null );
		$this->type = 'VOID';
		return $this->_sendRequest();
	}

	/**
	 * Do a CAPTURE_ONLY transaction.
	 *
	 * Required "x_" fields: auth_code, amount, card_num , exp_date
	 *
	 * @param string $auth_code The auth code
	 * @param string $amount    The dollar amount to charge
	 * @param string $card_num  The last 4 of credit card number
	 * @param string $exp_date  CC expiration date
	 *
	 * @return AuthorizeNetAIM_Response
	 */
	public function captureOnly( $auth_code = false, $amount = false, $card_num = false, $exp_date = false ) {
		( $auth_code ? $this->auth_code = $auth_code : null );
		( $amount ? $this->amount = $amount : null );
		( $card_num ? $this->card_num = $card_num : null );
		( $exp_date ? $this->exp_date = $exp_date : null );
		$this->type = 'CAPTURE_ONLY';
		return $this->_sendRequest();
	}

	/**
	 * Do a CREDIT transaction.
	 *
	 * Required "x_" fields: trans_id, amount, card_num (just the last 4)
	 *
	 * @param string $trans_id Transaction id to credit
	 * @param string $amount   The dollar amount to credit
	 * @param string $card_num The last 4 of credit card number
	 *
	 * @return AuthorizeNetAIM_Response
	 */
	public function credit( $trans_id = false, $amount = false, $card_num = false ) {
		( $trans_id ? $this->trans_id = $trans_id : null );
		( $amount ? $this->amount = $amount : null );
		( $card_num ? $this->card_num = $card_num : null );
		$this->type = 'CREDIT';
		return $this->_sendRequest();
	}

	/**
	 * Alternative syntax for setting x_ fields.
	 *
	 * Usage: $sale->method = "echeck";
	 *
	 * @param string $name
	 * @param string $value
	 */
	public function __set( $name, $value ) {
		$this->setField( $name, $value );
	}

	/**
	 * Quickly set multiple fields.
	 *
	 * Note: The prefix x_ will be added to all fields. If you want to set a
	 * custom field without the x_ prefix, use setCustomField or setCustomFields.
	 *
	 * @param array $fields Takes an array or object.
	 */
	public function setFields( $fields ) {
		$array = (array) $fields;
		foreach ( $array as $key => $value ) {
			$this->setField( $key, $value );
		}
	}

	/**
	 * Quickly set multiple custom fields.
	 *
	 * @param array $fields
	 */
	public function setCustomFields( $fields ) {
		$array = (array) $fields;
		foreach ( $array as $key => $value ) {
			$this->setCustomField( $key, $value );
		}
	}

	/**
	 * Add a line item.
	 *
	 * @param string $item_id
	 * @param string $item_name
	 * @param string $item_description
	 * @param string $item_quantity
	 * @param string $item_unit_price
	 * @param string $item_taxable
	 */
	public function addLineItem( $item_id, $item_name, $item_description, $item_quantity, $item_unit_price, $item_taxable ) {
		$line_item = '';
		$delimiter = '';
		foreach ( func_get_args() as $key => $value ) {
			$line_item .= $delimiter . $value;
			$delimiter  = '<|>';
		}
		$this->_additional_line_items[] = $line_item;
	}

	/**
	 * Use ECHECK as payment type.
	 */
	public function setECheck( $bank_aba_code, $bank_acct_num, $bank_acct_type, $bank_name, $bank_acct_name, $echeck_type = 'WEB' ) {
		$this->setFields(
			array(
				'method'         => 'echeck',
				'bank_aba_code'  => $bank_aba_code,
				'bank_acct_num'  => $bank_acct_num,
				'bank_acct_type' => $bank_acct_type,
				'bank_name'      => $bank_name,
				'bank_acct_name' => $bank_acct_type,
				'echeck_type'    => $echeck_type,
			)
		);
	}

	/**
	 * Set an individual name/value pair. This will append x_ to the name
	 * before posting.
	 *
	 * @param string $name
	 * @param string $value
	 */
	public function setField( $name, $value ) {
		if ( $this->verify_x_fields ) {
			if ( in_array( $name, $this->_all_aim_fields ) ) {
				$this->_x_post_fields[ $name ] = $value;
			} else {
				throw new AuthorizeNetException(
					"Error: no field $name exists in the AIM API.
                To set a custom field use setCustomField('field','value') instead."
				);
			}
		} else {
			$this->_x_post_fields[ $name ] = $value;
		}
	}

	/**
	 * Set a custom field. Note: the x_ prefix will not be added to
	 * your custom field if you use this method.
	 *
	 * @param string $name
	 * @param string $value
	 */
	public function setCustomField( $name, $value ) {
		$this->_custom_fields[ $name ] = $value;
	}

	/**
	 * Unset an x_ field.
	 *
	 * @param string $name Field to unset.
	 */
	public function unsetField( $name ) {
		unset( $this->_x_post_fields[ $name ] );
	}

	/**
	 *
	 *
	 * @param string $response
	 *
	 * @return AuthorizeNetAIM_Response
	 */
	protected function _handleResponse( $response ) {
		return new AuthorizeNetAIM_Response( $response, $this->_x_post_fields['delim_char'], $this->_x_post_fields['encap_char'], $this->_custom_fields );
	}

	/**
	 * @return string
	 */
	protected function _getPostUrl() {
		return ( $this->_sandbox ? self::SANDBOX_URL : self::LIVE_URL );
	}

	/**
	 * Converts the x_post_fields array into a string suitable for posting.
	 */
	protected function _setPostString() {
		$this->_x_post_fields['login']    = $this->_api_login;
		$this->_x_post_fields['tran_key'] = $this->_transaction_key;
		$this->_post_string               = '';
		foreach ( $this->_x_post_fields as $key => $value ) {
			$this->_post_string .= "x_$key=" . urlencode( $value ) . '&';
		}
		// Add line items
		foreach ( $this->_additional_line_items as $key => $value ) {
			$this->_post_string .= 'x_line_item=' . urlencode( $value ) . '&';
		}
		// Add custom fields
		foreach ( $this->_custom_fields as $key => $value ) {
			$this->_post_string .= "$key=" . urlencode( $value ) . '&';
		}
		$this->_post_string = rtrim( $this->_post_string, '& ' );
	}
}

/**
 * Parses an AuthorizeNet AIM Response.
 *
 * @package    AuthorizeNet
 * @subpackage AuthorizeNetAIM
 */
class AuthorizeNetAIM_Response extends AuthorizeNetResponse {

	private $_response_array = array(); // An array with the split response.

	/**
	 * Constructor. Parses the AuthorizeNet response string.
	 *
	 * @param string $response      The response from the AuthNet server.
	 * @param string $delimiter     The delimiter used (default is ",")
	 * @param string $encap_char    The encap_char used (default is "|")
	 * @param array  $custom_fields Any custom fields set in the request.
	 */
	public function __construct( $response, $delimiter, $encap_char, $custom_fields ) {
		if ( $response ) {

			// Split Array
			$this->response = $response;
			if ( $encap_char ) {
				$this->_response_array = explode( $encap_char . $delimiter . $encap_char, substr( $response, 1, -1 ) );
			} else {
				$this->_response_array = explode( $delimiter, $response );
			}

			/**
			 * If AuthorizeNet doesn't return a delimited response.
			 */
			if ( count( $this->_response_array ) < 10 ) {
				$this->approved      = false;
				$this->error         = true;
				$this->error_message = "Unrecognized response from AuthorizeNet: $response";
				return;
			}

			// Set all fields
			$this->response_code         = $this->_response_array[0];
			$this->response_subcode      = $this->_response_array[1];
			$this->response_reason_code  = $this->_response_array[2];
			$this->response_reason_text  = $this->_response_array[3];
			$this->authorization_code    = $this->_response_array[4];
			$this->avs_response          = $this->_response_array[5];
			$this->transaction_id        = $this->_response_array[6];
			$this->invoice_number        = $this->_response_array[7];
			$this->description           = $this->_response_array[8];
			$this->amount                = $this->_response_array[9];
			$this->method                = $this->_response_array[10];
			$this->transaction_type      = $this->_response_array[11];
			$this->customer_id           = $this->_response_array[12];
			$this->first_name            = $this->_response_array[13];
			$this->last_name             = $this->_response_array[14];
			$this->company               = $this->_response_array[15];
			$this->address               = $this->_response_array[16];
			$this->city                  = $this->_response_array[17];
			$this->state                 = $this->_response_array[18];
			$this->zip_code              = $this->_response_array[19];
			$this->country               = $this->_response_array[20];
			$this->phone                 = $this->_response_array[21];
			$this->fax                   = $this->_response_array[22];
			$this->email_address         = $this->_response_array[23];
			$this->ship_to_first_name    = $this->_response_array[24];
			$this->ship_to_last_name     = $this->_response_array[25];
			$this->ship_to_company       = $this->_response_array[26];
			$this->ship_to_address       = $this->_response_array[27];
			$this->ship_to_city          = $this->_response_array[28];
			$this->ship_to_state         = $this->_response_array[29];
			$this->ship_to_zip_code      = $this->_response_array[30];
			$this->ship_to_country       = $this->_response_array[31];
			$this->tax                   = $this->_response_array[32];
			$this->duty                  = $this->_response_array[33];
			$this->freight               = $this->_response_array[34];
			$this->tax_exempt            = $this->_response_array[35];
			$this->purchase_order_number = $this->_response_array[36];
			$this->md5_hash              = $this->_response_array[37];
			$this->card_code_response    = $this->_response_array[38];
			$this->cavv_response         = $this->_response_array[39];
			$this->account_number        = $this->_response_array[50];
			$this->card_type             = $this->_response_array[51];
			$this->split_tender_id       = $this->_response_array[52];
			$this->requested_amount      = $this->_response_array[53];
			$this->balance_on_card       = $this->_response_array[54];

			$this->approved = ( $this->response_code == self::APPROVED );
			$this->declined = ( $this->response_code == self::DECLINED );
			$this->error    = ( $this->response_code == self::ERROR );
			$this->held     = ( $this->response_code == self::HELD );

			// Set custom fields
			if ( $count = count( $custom_fields ) ) {
				$custom_fields_response = array_slice( $this->_response_array, -$count, $count );
				$i                      = 0;
				foreach ( $custom_fields as $key => $value ) {
					$this->$key = $custom_fields_response[ $i ];
					++$i;
				}
			}

			if ( $this->error ) {
				$this->error_message = 'AuthorizeNet Error:
                Response Code: ' . $this->response_code . '
                Response Subcode: ' . $this->response_subcode . '
                Response Reason Code: ' . $this->response_reason_code . '
                Response Reason Text: ' . $this->response_reason_text . '
                ';
			}
		} else {
			$this->approved      = false;
			$this->error         = true;
			$this->error_message = 'Error connecting to AuthorizeNet';
		}
	}
}
