<?php

/**
 * QuickBooks Merchant Service class
 *
 * Copyright (c) {2010-04-16} {Keith Palmer / ConsoliBYTE, LLC.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.opensource.org/licenses/eclipse-1.0.php
 *
 * QuickBooks Merchant Service enables online stores to charge credit cards and
 * debit cards via simple HTTPS POSTs to the QuickBooks Merchant Service
 * payment gateway. This class simplifies the process and wraps it in a nice
 * OOP interface.
 *
 * @author Keith Palmer <keith@consolibyte.com>
 * @license LICENSE.txt
 *
 * @package QuickBooks
 * @subpackage MerchantAccount
 */

/**
 * Utilities class (for masking and some other misc things)
 */
QuickBooks_Loader::load('/QuickBooks/Utilities.php');

/**
 * HTTP connection class
 */
QuickBooks_Loader::load('/QuickBooks/HTTP.php');

/**
 * QuickBooks driver factory for database logging
 */
QuickBooks_Loader::load('/QuickBooks/Driver/Factory.php');

/**
 * QuickBooks credit card class
 */
QuickBooks_Loader::load('/QuickBooks/MerchantService/CreditCard.php');

/**
 * QuickBooks checking account class
 */
QuickBooks_Loader::load('/QuickBooks/MerchantService/CheckingAccount.php');

/**
 * QuickBooks merchant service transaction class
 */
QuickBooks_Loader::load('/QuickBooks/MerchantService/Transaction.php');

/**
 * QuickBooks Merchant Service implementation
 */
class QuickBooks_MerchantService
{
	/**
	 * No error occurred
	 * @var integer
	 */
	const OK = QUICKBOOKS_ERROR_OK;

	/**
	 * No error occurred
	 * @var integer
	 */
	const ERROR_OK = QUICKBOOKS_ERROR_OK;

	/**
	 * Indicates a generic internal error
	 * @param integer
	 */
	const ERROR_INTERNAL = -1091;

	/**
	 * Indicates an error when parsing an XML stream
	 * @param integer
	 */
	const ERROR_XML = -1092;

	/**
	 * Indicates an error establishing a socket connection to QBMS
	 * @param integer
	 */
	const ERROR_SOCKET = -1093;

	/**
	 * Indicates an error with a parameter passed to QBMS
	 * @param integer
	 */
	const ERROR_PARAM = -1094;

	/**
	 * Indicates an internal SSL-related error
	 * @param integer
	 */
	const ERROR_SSL = -1095;

	/**
	 *
	 *
	 */
	const ERROR_HTTP = -1096;

	/**
	 * Indicates that this transaction type is a 'Charge' (actually capture funds on a credit/debit card)
	 * @var string
	 */
	const TYPE_CHARGE = 'Charge';

	/**
	 * Indicates that this transaction type is an 'Authorization' (hold funds, but don't actually charge the card)
	 * @var string
	 */
	const TYPE_AUTHORIZE = 'Authorize';

	/**
	 * Capture the authorized funds on a credit card
	 * @var string
	 */
	const TYPE_CAPTURE = 'Capture';

	/**
	 * Void a pending credit card authorization
	 * @var string
	 */
	const TYPE_VOID = 'Void';

	/**
	 * Refund a credit card payment
	 * @var string
	 */
	const TYPE_REFUND = 'Refund';

	/**
	 * Void or refund, depending on the transaction type/status
	 * @var string
	 */
	const TYPE_VOIDORREFUND = 'VoidOrRefund';

	const TYPE_WALLETADD = 'WalletAdd';

	const TYPE_WALLETMOD = 'WalletMod';

	const TYPE_WALLETDEL = 'WalletDelete';

	const TYPE_WALLETQUERY = 'WalletQuery';

	const TYPE_WALLETAUTHORIZE = 'WalletAuthorize';

	const TYPE_WALLETCHARGE = 'WalletCharge';

	const TYPE_CHECK_DEBIT = 'CheckDebit';

	/**
	 * Constant for the NotAvailable response some fields return
	 * @var string
	 */
	const NOTAVAILABLE = 'NotAvailable';
	const AVS_NOTAVAILABLE = 'NotAvailable';

	/**
	 * Constant to indicate success
	 * @var string
	 */
	const PASS = 'Pass';
	const AVS_PASS = 'Pass';

	const FAIL = 'Fail';
	const AVS_FAIL = 'Fail';

	const SEVERITY_INFO = 'INFO';

	const SEVERITY_WARN = 'WARN';

	const SEVERITY_ERROR = 'ERROR';

	const MODE_INTERNET = 'Internet';
	const MODE_TELEPHONE = 'Telephone';
	const MODE_SIGNED = 'SignedAuthOnFile';
	const MODE_MAILED = 'Mailed';
	const MODE_WITHRECEIPT = 'InPersonWithReceipt';
	const MODE_WITHOUTRECEIPT = 'InPersonNoReceipt';

	/*
	2000
	Authentication failed -- Invalid login name or password / certificate / ticket
	2010
	Unauthorized
	2020
	Session Authentication required
	2030
	Unsupported signon version
	2040
	Internal err

	*/

	/**
	 *
	 * Status OK, AVS Street and Zip fail, card security code fail
	 *
	 * IMPORTANT NOTE: This
	 */
	const TEST_AVSZIPCVVFAIL = 'configid=10000_avscvdfail';

	const TEST_COMMUNICATIONERROR = 'configid=10200_comm';

	/*
	<NameOnCard>configid=value </NameOnCard>
	Simply replace “value in the above line with one of the ConfigID values listeds in Table 6-2
	on page 44.
	Table 6-2 ConfigID values and the errors they generate:
	Error to be
	Returned ConfigID value to insert Error Emulated
	10200 10200_comm An error occurred while communicating with the credit
	card processing gateway.
	10201 10201_login An error occurred during login to the processing
	gateway.
	10301 10301_ccinvalid This credit card account number is invalid.
	10400 10400_insufffunds This account does not have sufficient funds to process
	this transaction.
	10401 10401_decline The request to process this transaction has been
	declined.
	10403 10403_acctinvalid The merchant account information submitted is not
	recognized.
	10404 10404_referral This transaction has been declined, but can be
	approved by obtaining a Voice Authorization code from
	the card issuer.
	10405 10405_void An error occurred while attempting to void this
	transaction.
	10406 10406_capture An error occurred while processing the capture
	transaction.
	10500 10500_general A general error occurred at the credit card processing
	gateway.
	*/

	/**
	 * The connection ticket used to connect to QuickBooks Merchant Service
	 * @var string
	 */
	protected $_connection_ticket;

	/**
	 * The application login used to log in
	 * @var string
	 */
	protected $_application_login;

	//protected $_app_name;

	//protected $_app_url;

	//protected $_app_id;

	//protected $_auth_id;

	protected $_certificate;

	//protected $_logfile = '/tmp/curlerrors.txt';

	protected $_driver;

	protected $_test;
	protected $_debug;
	protected $_masking;

	//Testing - https://webmerchantaccount.ptc.quickbooks.com/j/AppGateway
	//Debug - https://webmerchantaccount.quickbooks.com/j/diag/http

	protected $_live_gateway = 'https://webmerchantaccount.quickbooks.com/j/AppGateway';
	protected $_test_gateway = 'https://webmerchantaccount.ptc.quickbooks.com/j/AppGateway';

	protected $_ticket_session = '';
	protected $_ticket_connection = '';

	protected $_last_request;
	protected $_last_response;

	protected $_batch;

	protected $_errnum;
	protected $_errmsg;

	protected $_warnnum;
	protected $_warnmsg;

	/**
	 *
	 *
	 *
	 */
	public function __construct($dsn, $certificate, $application_login = null, $connection_ticket = null)
	{
		$this->_test = false;
		$this->_debug = false;

		// Mask credit card numbers
		$this->_masking = true;

		if ($dsn)
		{
			// @TODO Figure out a better way to set the logging level
			$driver_options = array();
			$driver_hooks = array();
			$driver_loglevel = QUICKBOOKS_LOG_DEVELOP;
			$this->_driver = QuickBooks_Driver_Factory::create($dsn, $driver_options, $driver_hooks, $driver_loglevel);
		}

		if ($application_login)
		{
			$this->_application_login = $application_login;
		}
		else if ($this->_driver)
		{

		}

		if ($connection_ticket)
		{
			$this->_ticket_connection = $connection_ticket;
		}
		else if ($this->_driver)
		{
			//$this->_ticket_connection = $this->_driver->
		}

		$this->_certificate = $certificate;

		$this->_errnum = QuickBooks_MerchantService::ERROR_OK;
		$this->_errmsg = '';

		$this->_warnnum = QuickBooks_MerchantService::ERROR_OK;
		$this->_warnmsg = '';
	}

	/**
	 * Get the error number of the last error that occured
	 *
	 * @return integer
	 */
	public function errorNumber()
	{
		return $this->_errnum;
	}

	/**
	 * Get the error message of the last error that occured
	 *
	 * @return string
	 */
	public function errorMessage()
	{
		return $this->_errmsg;
	}

	/**
	 * Get the error number of the last error that occured
	 *
	 * @return integer
	 */
	public function warningNumber()
	{
		return $this->_warnnum;
	}

	/**
	 * Get the error message of the last error that occured
	 *
	 * @return string
	 */
	public function warningMessage()
	{
		return $this->_warnmsg;
	}

	/**
	 * Get the last raw XML response that was received
	 *
	 * @return string
	 */
	public function lastResponse()
	{
		return $this->_last_response;
	}

	/**
	 * Get the last raw XML request that was sent
	 *
	 * @return string
	 */
	public function lastRequest()
	{
		return $this->_last_request;
	}

	/**
	 * Set an error message
	 *
	 * @param integer $errnum	The error number/code
	 * @param string $errmsg	The text error message
	 * @return void
	 */
	protected function _setError($errnum, $errmsg = '')
	{
		$this->_errnum = $errnum;
		$this->_errmsg = $errmsg;
	}

	/**
	 *
	 *
	 *
	 */
	protected function _setWarning($warnnum, $warnmsg = '')
	{
		$this->_warnnum = $warnnum;
		$this->_warnmsg = $warnmsg;
	}

	/**
	 *
	 * @TODO Implement this
	 */
	public function addListener()
	{

	}

	/**
	 * Set to TRUE to use the Intuit development environment (make sure you have registered your application as a DEVELOPMENT application!)
	 *
	 * @param boolean $yes_or_no
	 * @return void
	 */
	public function useTestEnvironment($yes_or_no)
	{
		$this->_test = (boolean) $yes_or_no;
	}

	/**
	 * Set to TRUE to use the Intuit live environment (make sure you have registered your application as a PRODUCTION application!)
	 *
	 * @param boolean $yes_or_no
	 * @return void
	 */
	public function useLiveEnvironment($yes_or_no)
	{
		$this->_test = ! (boolean) $yes_or_no;
	}

	/**
	 * If masking is enabled (default) then credit card numbers, connection tickets, and session tickets will be masked when output or logged
	 *
	 * @param boolean $yes_or_no
	 * @return void
	 */
	public function useMasking($yes_or_no)
	{
		$this->_masking = (boolean) $yes_or_no;
	}

	/**
	 * Turn debugging mode on or off
	 *
	 * Turning debugging mode on will result in a large amount of output being
	 * printed directly to stdout (the web browser or the console)
	 *
	 * @param boolean $yes_or_no
	 * @return void
	 */
	public function useDebugMode($yes_or_no)
	{
		$this->_debug = (boolean) $yes_or_no;
	}

	protected function _extractTagContents($tag, $data)
	{
		// SessionTicket
		if (false !== strpos($data, '<' . $tag . '>') and
			false !== strpos($data, '</' . $tag . '>'))
		{
			$data = strstr($data, '<' . $tag . '>');
			$end = strpos($data, '</' . $tag . '>');

			return substr($data, strlen($tag) + 2, $end - (strlen($tag) + 2));
		}

		return null;
	}

	protected function _extractAttribute($attr, $data, $which = 0)
	{
		if ($which == 1)
		{
			$spos = strpos($data, $attr . '="');
			$data = substr($data, $spos + strlen($attr));
		}

		if (false !== ($spos = strpos($data, $attr . '="')) and
			false !== ($epos = strpos($data, '"', $spos + strlen($attr) + 2)))
		{
			//print('start: ' . $spos . "\n");
			//print('end: ' . $epos . "\n");

			return substr($data, $spos + strlen($attr) + 2, $epos - $spos - strlen($attr) - 2);
		}

		return '';
	}

	/**
	 * Sign on to the QBMS service to fetch a session ticket
	 *
	 * @return boolean
	 */
	public function signOn()
	{
		$this->_setError(QuickBooks_MerchantService::ERROR_OK);

		$xml = '';
		$xml .= '<?xml version="1.0" ?>' . QUICKBOOKS_CRLF;
		$xml .= '<?qbmsxml version="4.1"?>' . QUICKBOOKS_CRLF;
		$xml .= '<QBMSXML>' . QUICKBOOKS_CRLF;
		$xml .= '	<SignonMsgsRq>' . QUICKBOOKS_CRLF;

		if ($this->_certificate)
		{
			$this->_log('Signing on a HOSTED QBMS application.', QUICKBOOKS_LOG_DEBUG);

			$xml .= '		<SignonAppCertRq>' . QUICKBOOKS_CRLF;
			$xml .= '			<ClientDateTime>' . date('Y-m-d\TH:i:s') . '</ClientDateTime>' . QUICKBOOKS_CRLF;
			$xml .= '			<ApplicationLogin>' . $this->_application_login . '</ApplicationLogin>' . QUICKBOOKS_CRLF;
			$xml .= '			<ConnectionTicket>' . $this->_ticket_connection . '</ConnectionTicket>' . QUICKBOOKS_CRLF;
			$xml .= '		</SignonAppCertRq>' . QUICKBOOKS_CRLF;
		}
		else
		{
			$this->_log('Signing on as a DESKTOP QBMS application.', QUICKBOOKS_LOG_DEBUG);

			$xml .= '		<SignonDesktopRq>' . QUICKBOOKS_CRLF;
			$xml .= '			<ClientDateTime>' . date('Y-m-d\TH:i:s') . '</ClientDateTime>' . QUICKBOOKS_CRLF;
			$xml .= '			<ApplicationLogin>' . $this->_application_login . '</ApplicationLogin>' . QUICKBOOKS_CRLF;
			$xml .= '			<ConnectionTicket>' . $this->_ticket_connection . '</ConnectionTicket>' . QUICKBOOKS_CRLF;
			$xml .= '		</SignonDesktopRq>' . QUICKBOOKS_CRLF;
		}

		$xml .= '	</SignonMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '</QBMSXML>' . QUICKBOOKS_CRLF;

		$errnum = QuickBooks_MerchantService::ERROR_OK;
		$errmsg = '';

		$response = $this->_request($xml, $errnum, $errmsg);

		if ($errnum)
		{
			$this->_setError(QuickBooks_MerchantService::ERROR_SOCKET, $errnum . ': ' . $errmsg);
			return false;
		}

		$code = $this->_extractAttribute('statusCode', $response);
		$message = $this->_extractAttribute('statusMessage', $response);
		$severity = $this->_extractAttribute('statusSeverity', $response);

		$this->_log('SignOn (initial) response: ' . $severity . '/' . $code . ': ' . $message, QUICKBOOKS_LOG_DEBUG);

		if ($code != QuickBooks_MerchantService::ERROR_OK)
		{
			$this->_setError($code, $message);
			return false;
		}

		if ($ticket = $this->_extractTagContents('SessionTicket', $response))
		{
			$this->_ticket_session = $ticket;

			return true;
		}

		$this->_setError(QuickBooks_MerchantService::ERROR_INTERNAL, 'Could not locate SessionTicket in response.');

		return false;
	}

	/**
	 * Tell whether or not you have fetched a session ticket and signed on
	 *
	 * @return boolean
	 */
	public function isSignedOn()
	{
		return strlen($this->_ticket_session) > 0;
	}

	/**
	 * Create a unique transaction requestID from a set of parameters
	 *
	 * @param string $type
	 * @param object $Obj
	 * @param float $amount
	 * @param boolean $force_new_transaction
	 * @return string
	 */
	protected function _transRequestID($type, $Obj, $amount, $force_new_transaction = true)
	{
		$rand = '';
		if ($force_new_transaction)
		{
			$rand = mt_rand() . microtime();
		}

		return md5($type . '-' . serialize($Obj) . '-' . $amount . '-' . $rand);
	}

	/**
	 * Do a QuickBooks Merchant Service request and fetch the response
	 *
	 * @param string $type
	 * @param string $path
	 * @param string $xml
	 * @param object $Creditcard
	 * @return QuickBooks_MerchantService_Transaction
	 */
	protected function _doQBMS($type, $path, $xml, $CreditCard = null, $Transaction = null)
	{
		$errnum = QuickBooks_MerchantService::ERROR_OK;
		$errmsg = '';
		$response = $this->_request($xml, $errnum, $errmsg);

		if ($errnum)
		{
			$this->_setError(QuickBooks_MerchantService::ERROR_SOCKET, $errnum . ': ' . $errmsg);
			return false;
		}

		$signon_code = $this->_extractAttribute('statusCode', $response, 0);
		$signon_message = '';
		$signon_severity = $this->_extractAttribute('statusSeverity', $response, 0);

		if ($signon_code != QuickBooks_MerchantService::ERROR_OK)
		{
			$signon_message = $this->_extractAttribute('statusMessage', $response, 0);
		}

		$this->_log('SignOn (with session) response: ' . $signon_severity . '/' . $signon_code . ': ' . $signon_message, QUICKBOOKS_LOG_DEBUG);

		if ($signon_code != QuickBooks_MerchantService::ERROR_OK)
		{
			$this->_setError($signon_code, $signon_message);
			return false;
		}

		$qbms_code = $this->_extractAttribute('statusCode', $response, 1);
		$qbms_message = $this->_extractAttribute('statusMessage', $response, 0);	// 0th instance, because QBMS doesn't return a statusMessage for the first sucessful request
		$qbms_severity = $this->_extractAttribute('statusSeverity', $response, 1);

		$this->_log('QBMS Response: ' . $qbms_severity . '/' . $qbms_code . ': ' . $qbms_message, QUICKBOOKS_LOG_DEBUG);

		//if ($qbms_code != QuickBooks_MerchantService::ERROR_OK)
		if (!$qbms_severity or $qbms_severity == QuickBooks_MerchantService::SEVERITY_ERROR)
		{
			$this->_setError($qbms_code, $qbms_message);
			return false;
		}
		else if ($qbms_severity == QuickBooks_MerchantService::SEVERITY_WARN)
		{
			$this->_setWarning($qbms_code, $qbms_message);
			// return false;		// DO NOT RETURN HERE (it's just a warning)
		}
		else if ($qbms_severity == QuickBooks_MerchantService::SEVERITY_INFO)
		{
			; // Do nothing...
		}
		else
		{
			// If we get here, something has gone really wrong... no statusSeverity code indicating pass/fail/warn was returned???
			$this->_setError(QuickBooks_MerchantService::ERROR_INTERNAL, 'Could not locate a statusSeverity="..." attribute in returned stream: ' . $response);
			return false;
		}

		// Create a transaction result
		$xml_errnum = 0;
		$xml_errmsg = '';
		if ($NewTransaction = $this->_parseResponse($type, $path, $response, $xml_errnum, $xml_errmsg))
		{
			if ($CreditCard instanceof QuickBooks_MerchantService_CreditCard)
			{
				$NewTransaction->setExtraData(
					$qbms_code,
					$qbms_message,
					$CreditCard->getNumber(true),
					$CreditCard->getExpirationMonth(),
					$CreditCard->getExpirationYear(),
					$CreditCard->getName(),
					$CreditCard->getAddress(),
					$CreditCard->getPostalCode());
			}
			else if ($Transaction instanceof QuickBooks_MerchantService_Transaction)
			{
				$tmp = $Transaction->toArray();

				$NewTransaction->setExtraData(
					$qbms_code,
					$qbms_message,
					$tmp['CreditCardTxnInfo_CreditCardTxnInputInfo_CreditCardNumber'],
					$tmp['CreditCardTxnInfo_CreditCardTxnInputInfo_ExpirationMonth'],
					$tmp['CreditCardTxnInfo_CreditCardTxnInputInfo_ExpirationYear'],
					$tmp['CreditCardTxnInfo_CreditCardTxnInputInfo_NameOnCard'],
					$tmp['CreditCardTxnInfo_CreditCardTxnInputInfo_CreditCardAddress'],
					$tmp['CreditCardTxnInfo_CreditCardTxnInputInfo_CreditCardPostalCode']
					);

				$NewTransaction->setAVSStreet($tmp['CreditCardTxnInfo_CreditCardTxnResultInfo_AVSStreet']);
				$NewTransaction->setAVSZip($tmp['CreditCardTxnInfo_CreditCardTxnResultInfo_AVSZip']);
				$NewTransaction->setCardSecurityCodeMatch($tmp['CreditCardTxnInfo_CreditCardTxnResultInfo_CardSecurityCodeMatch']);
			}

			return $NewTransaction;
		}

		$this->_setError(QuickBooks_MerchantService::ERROR_XML, $xml_errnum . ': ' . $xml_errmsg);
		return false;
	}

	public function debitCheck($CheckingAccount, $amount, $payment_mode, $check_number = null, $comment = null, $is_recurring = false, $is_mobile = false, $force_new_transaction = true)
	{
		/*
<CustomerCheckDebitRq>
	<TransRequestID >STRTYPE</TransRequestID> <!-- required -->
	<KeyEnteredCheckInfo> <!-- optional -->
		<RoutingNumber >STRTYPE</RoutingNumber> <!-- required -->
		<AccountNumber >STRTYPE</AccountNumber> <!-- required -->
		<CheckNumber >STRTYPE</CheckNumber> <!-- optional -->
		<!-- BEGIN OR -->
		<PersonalPaymentInfo> <!-- optional -->
		<!-- PersonalDebitAccountType may have one of the following values: Checking, Savings -->
		<PersonalDebitAccountType >ENUMTYPE</PersonalDebitAccountType> <!-- required -->
		<PayorFirstName >STRTYPE</PayorFirstName> <!-- required -->
		<PayorLastName >STRTYPE</PayorLastName> <!-- required -->
		</PersonalPaymentInfo>
		<!-- OR -->
		<BusinessPaymentInfo> <!-- optional -->
			<!-- BusinessDebitAccountType may have one of the following values: Checking, Savings -->
			<BusinessDebitAccountType >ENUMTYPE</BusinessDebitAccountType> <!-- required -->
			<PayorFirstName >STRTYPE</PayorFirstName> <!-- optional -->
			<PayorLastName >STRTYPE</PayorLastName> <!-- optional -->
		</BusinessPaymentInfo>
		<!-- END OR -->
	</KeyEnteredCheckInfo>

	<PayorPhoneNumber >STRTYPE</PayorPhoneNumber> <!-- optional -->
	<Amount >AMTTYPE</Amount> <!-- required -->
	<!-- PaymentMode may have one of the following values: Internet, Telephone, SignedAuthOnFile, Mailed, InPersonWithReceipt, InPersonNoReceipt -->
	<PaymentMode >ENUMTYPE</PaymentMode> <!-- required -->
	<PayeeName >STRTYPE</PayeeName> <!-- optional -->
	<BatchID >STRTYPE</BatchID> <!-- optional -->
	<IsRecurring >BOOLTYPE</IsRecurring> <!-- optional -->
	<Comment >STRTYPE</Comment> <!-- optional -->
</CustomerCheckDebitRq>

<CustomerCheckDebitRs statusCode="INTTYPE" statusSeverity="STRTYPE" statusMessage="STRTYPE">
	<CheckTransID >STRTYPE</CheckTransID> <!-- optional -->
	<CheckAuthorizationCode >STRTYPE</CheckAuthorizationCode> <!-- optional -->
	<TxnAuthorizationTime >DATETIMETYPE</TxnAuthorizationTime> <!-- optional -->
	<ClientTransID >STRTYPE</ClientTransID> <!-- optional -->
	<StatusDetail >STRTYPE</StatusDetail> <!-- optional -->
</CustomerCheckDebitRs>
*/

		$this->_setError(QuickBooks_MerchantService::ERROR_OK);
		$this->_log('debitCheck()', QUICKBOOKS_LOG_VERBOSE);

		if (!$this->isSignedOn())
		{
			$this->signOn();

			if ($this->errorNumber())
			{
				return false;
			}
		}

		$transRequestID = $this->_transRequestID(QuickBooks_MerchantService::TYPE_CHECK_DEBIT, $CheckingAccount, $amount, $force_new_transaction);

		$xml = '';
		$xml .= '<?xml version="1.0" encoding="utf-8"?>' . QUICKBOOKS_CRLF;
		$xml .= '<?qbmsxml version="4.1"?>' . QUICKBOOKS_CRLF;
		$xml .= '<QBMSXML>' . QUICKBOOKS_CRLF;
		$xml .= $this->_createSessionXML();
		$xml .= '	<QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '		<CustomerCheckDebitRq>' . QUICKBOOKS_CRLF;
		$xml .= '			<TransRequestID>' . $transRequestID . '</TransRequestID>' . QUICKBOOKS_CRLF;

		$xml .= $this->_createCheckingAccountXML($CheckingAccount, $check_number, $amount, $payment_mode);

		//<BatchID >STRTYPE</BatchID> <!-- optional -->

		if ($comment)
		{
			$xml .= '			<Comment>' . substr(QuickBooks_XML::encode($comment), 0, 500) . '</Comment>' . QUICKBOOKS_CRLF;
		}

		$xml .= '		</CustomerCheckDebitRq>' . QUICKBOOKS_CRLF;
		$xml .= '	</QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '</QBMSXML>' . QUICKBOOKS_CRLF;

		return $this->_doQBMS(QuickBooks_MerchantService::TYPE_CHECK_DEBIT, 'QBMSXML/QBMSXMLMsgsRs/CustomerCheckDebitRs', $xml, $CheckingAccount);
	}

	public function voidCheck()
	{

	}

	public function addCheckWallet()
	{

	}

	public function updateCheckWallet()
	{

	}

	public function deleteCheckWallet($customerID, $walletID)
	{

	}

	public function debitCheckWallet($customerID, $walletID, $amount)
	{

	}

	public function debit($Debit, $amount, $force_new_transaction = true)
	{

	}

	public function addWallet($customerID, $Card)
	{
		$this->_setError(QuickBooks_MerchantService::ERROR_OK);
		$this->_log('addWallet()', QUICKBOOKS_LOG_VERBOSE);

		if (!$this->isSignedOn())
		{
			$this->signOn();

			if ($this->errorNumber())
			{
				return false;
			}
		}

		/*
		<CustomerCreditCardWalletAddRq>
			<CustomerID >STRTYPE</CustomerID> <!-- required -->
			<CreditCardNumber >STRTYPE</CreditCardNumber> <!-- required -->
			<ExpirationMonth >INTTYPE</ExpirationMonth> <!-- required -->
			<ExpirationYear >INTTYPE</ExpirationYear> <!-- required -->
			<NameOnCard >STRTYPE</NameOnCard> <!-- optional -->
			<CreditCardAddress >STRTYPE</CreditCardAddress> <!-- optional -->
			<CreditCardPostalCode >STRTYPE</CreditCardPostalCode> <!-- optional -->
		</CustomerCreditCardWalletAddRq>

		<CustomerCreditCardWalletAddRs statusCode="INTTYPE" statusSeverity="STRTYPE" statusMessage="STRTYPE">
			<WalletEntryID >STRTYPE</WalletEntryID> <!-- optional -->
			<IsDuplicate >BOOLTYPE</IsDuplicate> <!-- optional -->
			<StatusDetail >STRTYPE</StatusDetail> <!-- optional -->
		</CustomerCreditCardWalletAddRs>
		*/

		$include_address_data = true;
		$include_amounts = false;
		$include_card_number = true;
		$include_card_cvv = false;

		$xml = '<?xml version="1.0" ?>' . QUICKBOOKS_CRLF;
		$xml .= '<?qbmsxml version="4.1"?>' . QUICKBOOKS_CRLF;
		$xml .= '<QBMSXML>' . QUICKBOOKS_CRLF;
		$xml .= $this->_createSessionXML();
		$xml .= '	<QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '		<CustomerCreditCardWalletAddRq>' . QUICKBOOKS_CRLF;
		$xml .= '			<CustomerID>' . QuickBooks_XML::encode($customerID) . '</CustomerID>' . QUICKBOOKS_CRLF;
		$xml .= $this->_createCreditCardXML($Card, null, null, false, false, false, false, $include_address_data, $include_amounts, $include_card_number, $include_card_cvv);
		$xml .= '		</CustomerCreditCardWalletAddRq>' . QUICKBOOKS_CRLF;
		$xml .= '	</QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '</QBMSXML>' . QUICKBOOKS_CRLF;

		return $this->_doQBMS(QuickBooks_MerchantService::TYPE_WALLETADD, 'QBMSXML/QBMSXMLMsgsRs/CustomerCreditCardWalletAddRs', $xml);
	}

	/**
	 * Update a wallet entry
	 *
	 * Note: This method *does not* allow you to update the *credit card
	 *  number*. This is used only for updating the data associated with that
	 * 	particular credit card (address, expiration date, etc.)
	 *
	 * @param string $walletID
	 * @param mixed $customerID
	 * @param QuickBooks_MerchantService_CreditCard $Card
	 * @return boolean
	 */
	public function updateWallet($customerID, $walletID, $Card)
	{
		$this->_setError(QuickBooks_MerchantService::ERROR_OK);
		$this->_log('updateWallet()', QUICKBOOKS_LOG_VERBOSE);

		if (!$this->isSignedOn())
		{
			$this->signOn();

			if ($this->errorNumber())
			{
				return false;
			}
		}

		$include_address_data = true;
		$include_amounts = false;
		$include_card_number = false;
		$include_card_cvv = false;

		$xml = '<?xml version="1.0" ?>' . QUICKBOOKS_CRLF;
		$xml .= '<?qbmsxml version="4.1"?>' . QUICKBOOKS_CRLF;
		$xml .= '<QBMSXML>' . QUICKBOOKS_CRLF;
		$xml .= $this->_createSessionXML();
		$xml .= '	<QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '		<CustomerCreditCardWalletModRq>' . QUICKBOOKS_CRLF;
		$xml .= '			<WalletEntryID>' . $walletID . '</WalletEntryID>' . QUICKBOOKS_CRLF;
		$xml .= '			<CustomerID>' . QuickBooks_XML::encode($customerID) . '</CustomerID>' . QUICKBOOKS_CRLF;
		$xml .= $this->_createCreditCardXML($Card, null, null, false, false, false, false, $include_address_data, $include_amounts, $include_card_number, $include_card_cvv);
		$xml .= '		</CustomerCreditCardWalletModRq>' . QUICKBOOKS_CRLF;
		$xml .= '	</QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '</QBMSXML>' . QUICKBOOKS_CRLF;

		return $this->_doQBMS(QuickBooks_MerchantService::TYPE_WALLETMOD, 'QBMSXML/QBMSXMLMsgsRs/CustomerCreditCardWalletModRs', $xml);
	}

	public function deleteWallet($customerID, $walletID)
	{
		$this->_setError(QuickBooks_MerchantService::ERROR_OK);
		$this->_log('deleteWallet()', QUICKBOOKS_LOG_VERBOSE);

		if (!$this->isSignedOn())
		{
			$this->signOn();

			if ($this->errorNumber())
			{
				return false;
			}
		}

		$xml = '<?xml version="1.0" ?>' . QUICKBOOKS_CRLF;
		$xml .= '<?qbmsxml version="4.1"?>' . QUICKBOOKS_CRLF;
		$xml .= '<QBMSXML>' . QUICKBOOKS_CRLF;
		$xml .= $this->_createSessionXML();
		$xml .= '	<QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '		<CustomerCreditCardWalletDelRq>' . QUICKBOOKS_CRLF;
		$xml .= '			<WalletEntryID>' . $walletID . '</WalletEntryID>' . QUICKBOOKS_CRLF;
		$xml .= '			<CustomerID>' . QuickBooks_XML::encode($customerID) . '</CustomerID>' . QUICKBOOKS_CRLF;
		$xml .= '		</CustomerCreditCardWalletDelRq>' . QUICKBOOKS_CRLF;
		$xml .= '	</QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '</QBMSXML>' . QUICKBOOKS_CRLF;

		return $this->_doQBMS(QuickBooks_MerchantService::TYPE_WALLETDEL, 'QBMSXML/QBMSXMLMsgsRs/CustomerCreditCardWalletDelRs', $xml);
	}

	public function authorizeWallet($customerID, $walletID, $amount, $salestax = null, $comment = null, $cvv = null, $is_ecommerce = true, $is_recurring = false, $is_mobile = false, $force_new_transaction = true)
	{
		$this->_setError(QuickBooks_MerchantService::ERROR_OK);
		$this->_log('authorizeWallet()', QUICKBOOKS_LOG_VERBOSE);

		if (!$this->isSignedOn())
		{
			$this->signOn();

			if ($this->errorNumber())
			{
				return false;
			}
		}

		// Error checking
		if (!is_numeric($amount))
		{
			$this->_setError(QuickBooks_MerchantService::ERROR_PARAM, 'authorizeWallet() expects second parameter to be a float, got: ' . print_r($amount, true));
			return false;
		}

		// Get the Card from the wallet so we can send the data to QuickBooks
		$Card = $this->getWallet($customerID, $walletID);

		$transRequestID = $this->_transRequestID(QuickBooks_MerchantService::TYPE_WALLETAUTHORIZE, null, $amount, $force_new_transaction);

		$xml = '';
		$xml .= '<?xml version="1.0" encoding="utf-8"?>' . QUICKBOOKS_CRLF;
		$xml .= '<?qbmsxml version="4.1"?>' . QUICKBOOKS_CRLF;
		$xml .= '<QBMSXML>' . QUICKBOOKS_CRLF;
		$xml .= $this->_createSessionXML();
		$xml .= '	<QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '		<CustomerCreditCardWalletAuthRq>' . QUICKBOOKS_CRLF;
		$xml .= '			<TransRequestID>' . $transRequestID . '</TransRequestID>' . QUICKBOOKS_CRLF;
		$xml .= '			<WalletEntryID>' . $walletID . '</WalletEntryID>' . QUICKBOOKS_CRLF;
		$xml .= '			<CustomerID>' . QuickBooks_XML::encode($customerID) . '</CustomerID>' . QUICKBOOKS_CRLF;

		// 				_createCreditCardXML($Card, $amount, $salestax, $is_card_present, $is_ecommerce, $is_recurring, $include_address_data = true, $include_amounts = true, $include_card_number = true, $include_card_cvv = true, $include_card_dates = true
		$xml .= $this->_createCreditCardXML(null, $amount, $salestax, false, $is_ecommerce, $is_recurring, $is_mobile, false, true, false, true, false);

		if ($cvv)
		{
			$xml .= '			<CardSecurityCode>' . $cvv . '</CardSecurityCode>' . QUICKBOOKS_CRLF;
		}

		//<BatchID >STRTYPE</BatchID> <!-- optional -->

		if ($comment)
		{
			$xml .= '			<Comment>' . substr(QuickBooks_XML::encode($comment), 0, 500) . '</Comment>' . QUICKBOOKS_CRLF;
		}

		$xml .= '		</CustomerCreditCardWalletAuthRq>' . QUICKBOOKS_CRLF;
		$xml .= '	</QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '</QBMSXML>' . QUICKBOOKS_CRLF;

		// We set $type to AUTHORIZE because this really is an authorization,
		//	and this $type gets output to the Transaction object and is view
		//	and checkable by the end-user, and possibly sent to QuickBooks.
		$type = QuickBooks_MerchantService::TYPE_AUTHORIZE;

		return $this->_doQBMS($type, 'QBMSXML/QBMSXMLMsgsRs/CustomerCreditCardWalletAuthRs', $xml, $Card);
	}

	public function chargeWallet($customerID, $walletID, $amount, $salestax = null, $comment = null, $cvv = null, $is_ecommerce = true, $is_recurring = false, $is_mobile = false, $force_new_transaction = true)
	{
		$this->_setError(QuickBooks_MerchantService::ERROR_OK);
		$this->_log('chargeWallet()', QUICKBOOKS_LOG_VERBOSE);

		if (!$this->isSignedOn())
		{
			$this->signOn();

			if ($this->errorNumber())
			{
				return false;
			}
		}

		// Error checking
		if (!is_numeric($amount))
		{
			$this->_setError(QuickBooks_MerchantService::ERROR_PARAM, 'authorizeCharge() expects second parameter to be a float, got: ' . print_r($amount, true));
			return false;
		}

		// Get the Card from the wallet so we can send the data to QuickBooks
		$Card = $this->getWallet($customerID, $walletID);

		$transRequestID = $this->_transRequestID(QuickBooks_MerchantService::TYPE_WALLETCHARGE, $Card, $amount, $force_new_transaction);

		$xml = '';
		$xml .= '<?xml version="1.0" encoding="utf-8"?>' . QUICKBOOKS_CRLF;
		$xml .= '<?qbmsxml version="4.1"?>' . QUICKBOOKS_CRLF;
		$xml .= '<QBMSXML>' . QUICKBOOKS_CRLF;
		$xml .= $this->_createSessionXML();
		$xml .= '	<QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '		<CustomerCreditCardWalletChargeRq>' . QUICKBOOKS_CRLF;
		$xml .= '			<TransRequestID>' . $transRequestID . '</TransRequestID>' . QUICKBOOKS_CRLF;
		$xml .= '			<WalletEntryID>' . $walletID . '</WalletEntryID>' . QUICKBOOKS_CRLF;
		$xml .= '			<CustomerID>' . QuickBooks_XML::encode($customerID) . '</CustomerID>' . QUICKBOOKS_CRLF;

		// 				_createCreditCardXML($Card, $amount, $salestax, $is_card_present, $is_ecommerce, $is_recurring, $include_address_data = true, $include_amounts = true, $include_card_number = true, $include_card_cvv = true, $include_card_dates = true
		$xml .= $this->_createCreditCardXML(null, $amount, $salestax, false, $is_ecommerce, $is_recurring, $is_mobile, false, true, false, true, false);

		if ($cvv)
		{
			$xml .= '			<CardSecurityCode>' . $cvv . '</CardSecurityCode>' . QUICKBOOKS_CRLF;
		}

		//<BatchID >STRTYPE</BatchID> <!-- optional -->

		if ($comment)
		{
			$xml .= '			<Comment>' . substr(QuickBooks_XML::encode($comment), 0, 500) . '</Comment>' . QUICKBOOKS_CRLF;
		}

		$xml .= '		</CustomerCreditCardWalletChargeRq>' . QUICKBOOKS_CRLF;
		$xml .= '	</QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '</QBMSXML>' . QUICKBOOKS_CRLF;

		// We set $type to CHARGE because this really is an authorization, and
		//	this $type gets output to the Transaction object and is view and
		//	checkable by the end-user, and possibly sent to QuickBooks.
		$type = QuickBooks_MerchantService::TYPE_CHARGE;

		return $this->_doQBMS($type, 'QBMSXML/QBMSXMLMsgsRs/CustomerCreditCardWalletChargeRs', $xml, $Card);
	}

	public function getWallet($customerID, $walletID)
	{
		$this->_setError(QuickBooks_MerchantService::ERROR_OK);
		$this->_log('getWallet()', QUICKBOOKS_LOG_VERBOSE);

		if (!$this->isSignedOn())
		{
			$this->signOn();

			if ($this->errorNumber())
			{
				return false;
			}
		}

		$xml = '<?xml version="1.0" ?>' . QUICKBOOKS_CRLF;
		$xml .= '<?qbmsxml version="4.1"?>' . QUICKBOOKS_CRLF;
		$xml .= '<QBMSXML>' . QUICKBOOKS_CRLF;
		$xml .= $this->_createSessionXML();
		$xml .= '	<QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '		<CustomerWalletQueryRq>' . QUICKBOOKS_CRLF;
		$xml .= '			<WalletEntryID>' . $walletID . '</WalletEntryID>' . QUICKBOOKS_CRLF;
		$xml .= '			<CustomerID>' . QuickBooks_XML::encode($customerID) . '</CustomerID>' . QUICKBOOKS_CRLF;
		$xml .= '		</CustomerWalletQueryRq>' . QUICKBOOKS_CRLF;
		$xml .= '	</QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '</QBMSXML>' . QUICKBOOKS_CRLF;

		return $this->_doQBMS(QuickBooks_MerchantService::TYPE_WALLETQUERY, 'QBMSXML/QBMSXMLMsgsRs/CustomerWalletQueryRs', $xml);
	}

	/**
	 *
	 *
	 */
	public function authorize($Card, $amount, $salestax = null, $comment = null, $is_card_present = false, $is_ecommerce = true, $is_recurring = false, $is_mobile = false, $force_new_transaction = true)
	{
		$this->_setError(QuickBooks_MerchantService::ERROR_OK);
		$this->_log('authorize()', QUICKBOOKS_LOG_VERBOSE);

		if (!$this->isSignedOn())
		{
			$this->signOn();

			if ($this->errorNumber())
			{
				return false;
			}
		}

		// Error checking
		if (!($Card instanceof QuickBooks_MerchantService_CreditCard))
		{
			$this->_setError(QuickBooks_MerchantService::ERROR_PARAM, 'authorize() expects first parameter to be a Card object, got: ' . print_r($Card, true));
			return false;
		}

		if (!is_numeric($amount))
		{
			$this->_setError(QuickBooks_MerchantService::ERROR_PARAM, 'authorize() expects second parameter to be a float, got: ' . print_r($amount, true));
			return false;
		}

		$transRequestID = $this->_transRequestID(QuickBooks_MerchantService::TYPE_AUTHORIZE, $Card, $amount, $force_new_transaction);

		$xml = '<?xml version="1.0" ?>' . QUICKBOOKS_CRLF;
		$xml .= '<?qbmsxml version="4.1"?>' . QUICKBOOKS_CRLF;
		$xml .= '<QBMSXML>' . QUICKBOOKS_CRLF;
		$xml .= $this->_createSessionXML();
		$xml .= '	<QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '		<CustomerCreditCardAuthRq>' . QUICKBOOKS_CRLF;
		$xml .= '			<TransRequestID>' . $transRequestID . '</TransRequestID>' . QUICKBOOKS_CRLF;
		$xml .= $this->_createCreditCardXML($Card, $amount, $salestax, $is_card_present, $is_ecommerce, $is_recurring, $is_mobile);
		$xml .= '		</CustomerCreditCardAuthRq>' . QUICKBOOKS_CRLF;
		$xml .= '	</QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '</QBMSXML>' . QUICKBOOKS_CRLF;

		return $this->_doQBMS(QuickBooks_MerchantService::TYPE_AUTHORIZE, 'QBMSXML/QBMSXMLMsgsRs/CustomerCreditCardAuthRs', $xml, $Card);
	}

	protected function _createSessionXML()
	{
		$xml = '';

		$xml .= '	<SignonMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '		<SignonTicketRq>' . QUICKBOOKS_CRLF;
		$xml .= '			<ClientDateTime>' . date('Y-m-d\TH:i:s') . '</ClientDateTime>' . QUICKBOOKS_CRLF;
		$xml .= '			<SessionTicket>' . $this->_ticket_session . '</SessionTicket>' . QUICKBOOKS_CRLF;
		$xml .= '		</SignonTicketRq>' . QUICKBOOKS_CRLF;
		$xml .= '	</SignonMsgsRq>' . QUICKBOOKS_CRLF;

		return $xml;
	}

	protected function _parseResponse($type, $path, $xml, &$errnum, &$errmsg)
	{
		static $look_trans = array(
			'CreditCardTransID' => null,
			'AuthorizationCode' => null,
			'AVSStreet' => null,
			'AVSZip' => null,
			'CardSecurityCodeMatch' => null,
			'ClientTransID' => null,
			'MerchantAccountNumber' => null,
			'ReconBatchID' => null,
			'PaymentGroupingCode' => null,
			'PaymentStatus' => null,
			'TxnAuthorizationTime' => null,
			'TxnAuthorizationStamp' => null,
			'NetworkName' => null,
			'NetworkNumber' => null,
			'DebitCardTransID' => null,
			);

		static $look_card = array(
			'MaskedCreditCardNumber' => null,
			'ExpirationMonth' => null,
			'ExpirationYear' => null,
			'NameOnCard' => null,
			'CreditCardAddress' => null,
			'CreditCardPostalCode' => null,
			);

		if ($type == QuickBooks_MerchantService::TYPE_WALLETADD)
		{
			// This was a wallet add, return the wallet ID
			return QuickBooks_XML::extractTagContents('WalletEntryID', $xml);
		}
		else if ($type == QuickBooks_MerchantService::TYPE_WALLETMOD or
			$type == QuickBooks_MerchantService::TYPE_WALLETDEL)
		{
			// These just return TRUE or FALSE based on error code
			return true;
		}
		else if ($type == QuickBooks_MerchantService::TYPE_WALLETQUERY)
		{
			// This returns a single credit card object
			$Parser = new QuickBooks_XML_Parser($xml);
			if ($Doc = $Parser->parse($errnum, $errmsg))
			{
				$Node = $Doc->getRoot();

				$card = array();
				foreach ($look_card as $node => $null)
				{
					$card[$node] = $Node->getChildDataAt($path . '/' . $node);
				}

				return new QuickBooks_MerchantService_CreditCard(
					$card['NameOnCard'],
					str_replace('*', 'x', $card['MaskedCreditCardNumber']),
					$card['ExpirationYear'],
					$card['ExpirationMonth'],
					$card['CreditCardAddress'],
					$card['CreditCardPostalCode']);
			}
		}
		else
		{
			$Parser = new QuickBooks_XML_Parser($xml);
			if ($Doc = $Parser->parse($errnum, $errmsg))
			{
				$Node = $Doc->getRoot();

				$trans = array();
				foreach ($look_trans as $node => $null)
				{
					$trans[$node] = $Node->getChildDataAt($path . '/' . $node);
				}

				//
				if ($trans['DebitCardTransID'])
				{
					$trans['CreditCardTransID'] = $trans['DebitCardTransID'];
				}

				return new QuickBooks_MerchantService_Transaction(
					$type,
					$trans['CreditCardTransID'],
					$trans['ClientTransID'],
					$trans['AuthorizationCode'],
					$trans['MerchantAccountNumber'],
					$trans['ReconBatchID'],
					$trans['PaymentGroupingCode'],
					$trans['PaymentStatus'],
					$trans['TxnAuthorizationTime'],
					$trans['TxnAuthorizationStamp'],
					$trans['AVSStreet'],
					$trans['AVSZip'],
					$trans['CardSecurityCodeMatch'],
					$trans['NetworkName'],
					$trans['NetworkNumber']);
			}
		}

		return false;
	}

	protected function _createCheckingAccountXML($CheckingAccount, $check_number, $amount, $payment_mode)
	{
		$xml = '';

		if ($CheckingAccount)
		{
			$xml .= '<KeyEnteredCheckInfo>' . QUICKBOOKS_CRLF;
			$xml .= "\t" . '<RoutingNumber>' . $CheckingAccount->getRoutingNumber() . '</RoutingNumber>' . QUICKBOOKS_CRLF;
			$xml .= "\t" . '<AccountNumber>' . $CheckingAccount->getAccountNumber() . '</AccountNumber>' . QUICKBOOKS_CRLF;

			if ($check_number)
			{
				$xml .= "\t" . '<CheckNumber>' . $check_number . '</CheckNumber>' . QUICKBOOKS_CRLF;
			}

			if ($CheckingAccount->getInfo() == QuickBooks_MerchantService_CheckingAccount::INFO_PERSONAL)
			{
				$xml .= "\t" . '<PersonalPaymentInfo>' . QUICKBOOKS_CRLF;
				// <!-- PersonalDebitAccountType may have one of the following values: Checking, Savings -->
				$xml .= "\t" . "\t" . '<PersonalDebitAccountType>' . $CheckingAccount->getType() . '</PersonalDebitAccountType>' . QUICKBOOKS_CRLF;
				$xml .= "\t" . "\t" . '<PayorFirstName>' . htmlspecialchars($CheckingAccount->getFirstName(), ENT_QUOTES) . '</PayorFirstName>' . QUICKBOOKS_CRLF;
				$xml .= "\t" . "\t" . '<PayorLastName>' . htmlspecialchars($CheckingAccount->getLastName(), ENT_QUOTES) . '</PayorLastName>' . QUICKBOOKS_CRLF;
				$xml .= "\t" . '</PersonalPaymentInfo>' . QUICKBOOKS_CRLF;
			}
			else
			{
				$xml .= "\t" . '<BusinessPaymentInfo>' . QUICKBOOKS_CRLF;
				// <!-- BusinessDebitAccountType may have one of the following values: Checking, Savings -->
				$xml .= "\t" . "\t" . '<BusinessDebitAccountType>' . $CheckingAccount->getType() . '</BusinessDebitAccountType>' . QUICKBOOKS_CRLF;
				$xml .= "\t" . "\t" . '<PayorFirstName>' . htmlspecialchars($CheckingAccount->getFirstName(), ENT_QUOTES) . '</PayorFirstName>' . QUICKBOOKS_CRLF;
				$xml .= "\t" . "\t" . '<PayorLastName>' . htmlspecialchars($CheckingAccount->getLastName(), ENT_QUOTES) . '</PayorLastName>' . QUICKBOOKS_CRLF;
				$xml .= "\t" . '</BusinessPaymentInfo>' . QUICKBOOKS_CRLF;
			}

			$xml .= '</KeyEnteredCheckInfo>' . QUICKBOOKS_CRLF;
		}

		$xml .= '<PayorPhoneNumber>' . $CheckingAccount->getPhone() . '</PayorPhoneNumber>' . QUICKBOOKS_CRLF;
		$xml .= '<Amount>' . sprintf('%01.2f', $amount) . '</Amount>' . QUICKBOOKS_CRLF;
		// <!-- PaymentMode may have one of the following values: Internet, Telephone, SignedAuthOnFile, Mailed, InPersonWithReceipt, InPersonNoReceipt -->
		$xml .= '<PaymentMode>' . $payment_mode . '</PaymentMode>' . QUICKBOOKS_CRLF;

		// <PayeeName >STRTYPE</PayeeName> <!-- optional -->
		// <BatchID >STRTYPE</BatchID> <!-- optional -->
		// <IsRecurring >BOOLTYPE</IsRecurring> <!-- optional -->

		return $xml;
	}

	protected function _createCreditCardXML($Card, $amount, $salestax, $is_card_present, $is_ecommerce, $is_recurring, $is_mobile, $include_address_data = true, $include_amounts = true, $include_card_number = true, $include_card_cvv = true, $include_card_dates = true)
	{
		$xml = '';

		$is_track2_data = false;

		if ($is_track2_data)
		{
			//<Track2Data >STRTYPE</Track2Data> <!-- optional -->
		}
		else
		{
			if ($Card and
				$include_card_number)
			{
				$xml .= '			<CreditCardNumber>' . $Card->getNumber() . '</CreditCardNumber>' . QUICKBOOKS_CRLF;
			}

			if ($Card and
				$include_card_dates)
			{
				$xml .= '			<ExpirationMonth>' . $Card->getExpirationMonth() . '</ExpirationMonth>' . QUICKBOOKS_CRLF;
				$xml .= '			<ExpirationYear>' . $Card->getExpirationYear() . '</ExpirationYear>' . QUICKBOOKS_CRLF;
			}

			if ($is_ecommerce)
			{
				$xml .= '			<IsECommerce>true</IsECommerce>' . QUICKBOOKS_CRLF;
			}
			else
			{
				$xml .= '			<IsECommerce>false</IsECommerce>' . QUICKBOOKS_CRLF;
			}

			if (is_null($is_mobile))
			{

			}
			else if ($is_mobile)
			{
				$xml .= '			<IsMobile>true</IsMobile>' . QUICKBOOKS_CRLF;
			}
			else
			{
				$xml .= '			<IsMobile>false</IsMobile>' . QUICKBOOKS_CRLF;
			}

			if ($is_recurring)
			{

			}

			//$xml .= '			<IsCardPresent>BOOLTYPE</IsCardPresent>' . QUICKBOOKS_CRLF;
			//$xml .= '			<IsECommerce >BOOLTYPE</IsECommerce>' . QUICKBOOKS_CRLF;
			//$xml .= '			<IsRecurring >BOOLTYPE</IsRecurring>' . QUICKBOOKS_CRLF;
		}

		if ($include_amounts)
		{
			$xml .= '			<Amount>' . sprintf('%01.2f', (float) $amount) . '</Amount>' . QUICKBOOKS_CRLF;
		}

		if ($Card)
		{
			$xml .= '			<NameOnCard>' . substr(htmlspecialchars($Card->getName()), 0, 30) . '</NameOnCard>' . QUICKBOOKS_CRLF;
		}

		if ($include_address_data)
		{
			$xml .= '			<CreditCardAddress>' . substr(htmlspecialchars($Card->getAddress()), 0, 30) . '</CreditCardAddress>' . QUICKBOOKS_CRLF;
			$xml .= '			<CreditCardPostalCode>' . substr(str_replace(array('-', ' ', '.'), '', $Card->getPostalCode()), 0, 9) . '</CreditCardPostalCode>' . QUICKBOOKS_CRLF;
		}

		//$xml .= '			<CommercialCardCode >STRTYPE</CommercialCardCode>' . QUICKBOOKS_CRLF;

		if ($include_amounts and
			!is_null($salestax))
		{
			$xml .= '			<SalesTaxAmount>' . sprintf('%01.2f', (float) $salestax) . '</SalesTaxAmount>' . QUICKBOOKS_CRLF;
		}

		if ($Card and
			$Card->getCVVCode() and
			$include_card_cvv)
		{
			$xml .= '			<CardSecurityCode>' . $Card->getCVVCode() . '</CardSecurityCode>' . QUICKBOOKS_CRLF;
		}

		/*
		<Lodging>
		<FolioID >STRTYPE</FolioID> <!-- required -->
		<!-- ChargeType may have one of the following values: ConventionFees, GiftShop, Golf, HealthClub, Hotel, Restaurant, Salon, Tennis -->
		<ChargeType >ENUMTYPE</ChargeType> <!-- optional -->
		<CheckInDate >DATETYPE</CheckInDate> <!-- optional -->
		<CheckOutDate >DATETYPE</CheckOutDate> <!-- optional -->
		<LengthOfStay >INTTYPE</LengthOfStay> <!-- optional -->
		<RoomRate >AMTTYPE</RoomRate> <!-- optional -->
		<!-- ExtraCharge may have one of the following values: GiftShop, Laundry, MiniBar, Restaurant, Telephone, Other -->
		<ExtraCharge >ENUMTYPE</ExtraCharge> <!-- must occur 0 - 6 times -->
		<!-- SpecialProgram may have one of the following values: AdvanceDeposit, AssuredReservation, DelayedCharge, ExpressService, NormalCharge, NoShowCharge -->
		<SpecialProgram >ENUMTYPE</SpecialProgram> <!-- optional -->
		<TotalAuthAmount >AMTTYPE</TotalAuthAmount> <!-- optional -->
		</Lodging>
		<Restaurant> <!-- optional -->
		<ServerID >STRTYPE</ServerID> <!-- optional -->
		<FoodAmount >AMTTYPE</FoodAmount> <!-- optional -->
		<BeverageAmount >AMTTYPE</BeverageAmount> <!-- optional -->
		<TaxAmount >AMTTYPE</TaxAmount> <!-- optional -->
		<TipAmount >AMTTYPE</TipAmount> <!-- optional -->
		</Restaurant>
		*/

		return $xml;
	}

	/**
	 *
	 *
	 *
	 *
	 */
	public function charge($Card, $amount, $salestax = null, $comment = null, $is_card_present = false, $is_ecommerce = true, $is_recurring = false, $is_mobile = false, $force_new_transaction = true)
	{
		$this->_setError(QuickBooks_MerchantService::ERROR_OK);
		$this->_log('charge()', QUICKBOOKS_LOG_VERBOSE);

		if (!$this->isSignedOn())
		{
			$this->signOn();

			if ($this->errorNumber())
			{
				return false;
			}
		}

		$transRequestID = $this->_transRequestID(QuickBooks_MerchantService::TYPE_CHARGE, $Card, $amount, $force_new_transaction);

		$xml = '';
		$xml .= '<?xml version="1.0" encoding="utf-8"?>' . QUICKBOOKS_CRLF;
		$xml .= '<?qbmsxml version="4.1"?>' . QUICKBOOKS_CRLF;
		$xml .= '<QBMSXML>' . QUICKBOOKS_CRLF;
		$xml .= $this->_createSessionXML();
		$xml .= '	<QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '		<CustomerCreditCardChargeRq>' . QUICKBOOKS_CRLF;
		$xml .= '			<TransRequestID>' . $transRequestID . '</TransRequestID>' . QUICKBOOKS_CRLF;

		$xml .= $this->_createCreditCardXML($Card, $amount, $salestax, $is_card_present, $is_ecommerce, $is_recurring, $is_mobile);

		//<BatchID >STRTYPE</BatchID> <!-- optional -->

		if ($comment)
		{
			$xml .= '			<Comment>' . substr(QuickBooks_XML::encode($comment), 0, 500) . '</Comment>' . QUICKBOOKS_CRLF;
		}

		$xml .= '		</CustomerCreditCardChargeRq>' . QUICKBOOKS_CRLF;
		$xml .= '	</QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '</QBMSXML>' . QUICKBOOKS_CRLF;

		return $this->_doQBMS(QuickBooks_MerchantService::TYPE_CHARGE, 'QBMSXML/QBMSXMLMsgsRs/CustomerCreditCardChargeRs', $xml, $Card);
	}

	/**
	 *
	 *
	 * @param QuickBooks_MerchantService_Transaction $Transaction
	 * @param float $amount
	 * @return QuickBooks_MerchantService_Transaction
	 */
	public function capture($Transaction, $amount = null, $comment = null, $force_new_transaction = true)
	{
		$this->_setError(QuickBooks_MerchantService::ERROR_OK);
		$this->_log('capture()', QUICKBOOKS_LOG_VERBOSE);

		// Fetch a session ticket if we havn't already
		if (!$this->isSignedOn())
		{
			$this->signOn();

			if ($this->errorNumber())
			{
				return false;
			}
		}

		// Error checking
		if (!($Transaction instanceof QuickBooks_MerchantService_Transaction))
		{
			$this->_setError(QuickBooks_MerchantService::ERROR_PARAM, 'capture() expects first parameter to be a Transaction object, got: ' . print_r($Transaction, true));
			return false;
		}

		if (!is_numeric($amount))
		{
			$this->_setError(QuickBooks_MerchantService::ERROR_PARAM, 'capture() expects second parameter to be a float, got: ' . print_r($amount, true));
			return false;
		}

		$transRequestID = $this->_transRequestID(QuickBooks_MerchantService::TYPE_CHARGE, $Transaction, $amount, $force_new_transaction);

		$xml = '';
		$xml .= '<?xml version="1.0" encoding="utf-8"?>' . QUICKBOOKS_CRLF;
		$xml .= '<?qbmsxml version="4.1"?>' . QUICKBOOKS_CRLF;
		$xml .= '<QBMSXML>' . QUICKBOOKS_CRLF;
		$xml .= $this->_createSessionXML();
		$xml .= '	<QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '		<CustomerCreditCardCaptureRq>' . QUICKBOOKS_CRLF;
		$xml .= '			<TransRequestID>' . $transRequestID . '</TransRequestID>' . QUICKBOOKS_CRLF;
		$xml .= '			<CreditCardTransID>' . $Transaction->getTransactionID() . '</CreditCardTransID>' . QUICKBOOKS_CRLF;

		if ((float) $amount)
		{
			$xml .= '			<Amount>' . sprintf('%01.2f', (float) $amount) . '</Amount>' . QUICKBOOKS_CRLF;
		}

		//<BatchID >STRTYPE</BatchID> <!-- optional -->

		if ($comment)
		{
			$xml .= '			<Comment>' . substr(QuickBooks_XML::encode($comment), 0, 500) . '</Comment>' . QUICKBOOKS_CRLF;
		}

		$xml .= '		</CustomerCreditCardCaptureRq>' . QUICKBOOKS_CRLF;
		$xml .= '	</QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '</QBMSXML>' . QUICKBOOKS_CRLF;

		return $this->_doQBMS(QuickBooks_MerchantService::TYPE_CAPTURE, 'QBMSXML/QBMSXMLMsgsRs/CustomerCreditCardCaptureRs', $xml, null, $Transaction);
	}

	/**
	 * Refund a credit card a given amount
	 *
	 *
	 */
	public function refund($Card, $amount, $salestax = null, $comment = null, $is_card_present = false, $is_ecommerce = true, $force_new_transaction = true)
	{
		$this->_setError(QuickBooks_MerchantService::ERROR_OK);
		$this->_log('refund()', QUICKBOOKS_LOG_VERBOSE);

		if (!$this->isSignedOn())
		{
			$this->signOn();

			if ($this->errorNumber())
			{
				return false;
			}
		}

		// Error checking
		if (!($Card instanceof QuickBooks_MerchantService_CreditCard))
		{
			$this->_setError(QuickBooks_MerchantService::ERROR_PARAM, 'refund() expects first parameter to be a Card object, got: ' . print_r($Card, true));
			return false;
		}

		if (!is_numeric($amount))
		{
			$this->_setError(QuickBooks_MerchantService::ERROR_PARAM, 'refund() expects second parameter to be a float, got: ' . print_r($amount, true));
			return false;
		}

		$transRequestID = $this->_transRequestID(QuickBooks_MerchantService::TYPE_REFUND, $Card, $amount, $force_new_transaction);

		$xml = '';
		$xml .= '<?xml version="1.0" encoding="utf-8"?>' . QUICKBOOKS_CRLF;
		$xml .= '<?qbmsxml version="4.1"?>' . QUICKBOOKS_CRLF;
		$xml .= '<QBMSXML>' . QUICKBOOKS_CRLF;
		$xml .= $this->_createSessionXML();
		$xml .= '	<QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '		<CustomerCreditCardRefundRq>' . QUICKBOOKS_CRLF;
		$xml .= '			<TransRequestID>' . $transRequestID . '</TransRequestID>' . QUICKBOOKS_CRLF;

		//                                  $Card, $amount, $salestax, $is_card_present, $is_ecommerce, $is_recurring, $is_mobile, $include_address_data = true, $include_amounts = true, $include_card_number = true, $include_card_cvv = true, $include_card_dates = true
		$xml .= $this->_createCreditCardXML($Card, $amount, $salestax, $is_card_present, $is_ecommerce, false,         null, false,                        true,                    true,                        false);

		//<BatchID >STRTYPE</BatchID> <!-- optional -->

		if ($comment)
		{
			$xml .= '			<Comment>' . substr(QuickBooks_XML::encode($comment), 0, 500) . '</Comment>' . QUICKBOOKS_CRLF;
		}

		$xml .= '		</CustomerCreditCardRefundRq>' . QUICKBOOKS_CRLF;
		$xml .= '	</QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '</QBMSXML>' . QUICKBOOKS_CRLF;

		return $this->_doQBMS(QuickBooks_MerchantService::TYPE_REFUND, 'QBMSXML/QBMSXMLMsgsRs/CustomerCreditCardRefundRs', $xml, $Card);
	}

	/**
	 *
	 *
	 *
	 */
	public function void($Transaction, $comment = null, $force_new_transaction = true)
	{
		$this->_setError(QuickBooks_MerchantService::ERROR_OK);
		$this->_log('void()', QUICKBOOKS_LOG_VERBOSE);

		if (!$this->isSignedOn())
		{
			$this->signOn();

			if ($this->errorNumber())
			{
				return false;
			}
		}

		// Error checking
		if (!($Transaction instanceof QuickBooks_MerchantService_Transaction))
		{
			$this->_setError(QuickBooks_MerchantService::ERROR_PARAM, 'void() expects first parameter to be a Transaction object, got: ' . print_r($Transaction, true));
			return false;
		}

		$transRequestID = $this->_transRequestID(QuickBooks_MerchantService::TYPE_VOID, $Transaction, null, $force_new_transaction);

		$xml = '';
		$xml .= '<?xml version="1.0" encoding="utf-8"?>' . QUICKBOOKS_CRLF;
		$xml .= '<?qbmsxml version="4.1"?>' . QUICKBOOKS_CRLF;
		$xml .= '<QBMSXML>' . QUICKBOOKS_CRLF;
		$xml .= $this->_createSessionXML();
		$xml .= '	<QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '		<CustomerCreditCardTxnVoidRq>' . QUICKBOOKS_CRLF;
		$xml .= '			<TransRequestID>' . $transRequestID . '</TransRequestID>' . QUICKBOOKS_CRLF;
		$xml .= '			<CreditCardTransID>' . $Transaction->getTransactionID() . '</CreditCardTransID>' . QUICKBOOKS_CRLF;

		if ($comment)
		{
			$xml .= '			<Comment>' . substr(QuickBooks_XML::encode($comment), 0, 500) . '</Comment>' . QUICKBOOKS_CRLF;
		}

		$xml .= '		</CustomerCreditCardTxnVoidRq>' . QUICKBOOKS_CRLF;
		$xml .= '	</QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '</QBMSXML>' . QUICKBOOKS_CRLF;

		return $this->_doQBMS(QuickBooks_MerchantService::TYPE_VOID, 'QBMSXML/QBMSXMLMsgsRs/CustomerCreditCardTxnVoidRs', $xml);
	}

	/**
	 *
	 *
	 *
	 */
	public function voidOrRefund($Transaction, $amount = null, $salestax = null, $comment = null, $force_new_transaction = true)
	{
		$this->_setError(QuickBooks_MerchantService::ERROR_OK);
		$this->_log('voidOrRefund()', QUICKBOOKS_LOG_VERBOSE);

		if (!$this->isSignedOn())
		{
			$this->signOn();

			if ($this->errorNumber())
			{
				return false;
			}
		}

		// Error checking
		if (!($Transaction instanceof QuickBooks_MerchantService_Transaction))
		{
			$this->_setError(QuickBooks_MerchantService::ERROR_PARAM, 'voidOrRefund() expects first parameter to be a Transaction object, got: ' . print_r($Transaction, true));
			return false;
		}

		if (!is_numeric($amount))
		{
			$this->_setError(QuickBooks_MerchantService::ERROR_PARAM, 'voidOrRefund() expects second parameter to be a float, got: ' . print_r($amount, true));
			return false;
		}

		$transRequestID = $this->_transRequestID(QuickBooks_MerchantService::TYPE_VOIDORREFUND, $Transaction, $amount, $force_new_transaction);

		$xml = '';
		$xml .= '<?xml version="1.0" encoding="utf-8"?>' . QUICKBOOKS_CRLF;
		$xml .= '<?qbmsxml version="4.1"?>' . QUICKBOOKS_CRLF;
		$xml .= '<QBMSXML>' . QUICKBOOKS_CRLF;
		$xml .= $this->_createSessionXML();
		$xml .= '	<QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '		<CustomerCreditCardTxnVoidOrRefundRq>' . QUICKBOOKS_CRLF;
		$xml .= '			<TransRequestID>' . $transRequestID . '</TransRequestID>' . QUICKBOOKS_CRLF;
		$xml .= '			<CreditCardTransID>' . $Transaction->getTransactionID() . '</CreditCardTransID>' . QUICKBOOKS_CRLF;

		if ((float) $amount)
		{
			$xml .= '			<Amount>' . sprintf('%01.2f', (float) $amount) . '</Amount>' . QUICKBOOKS_CRLF;
		}

		if (!is_null($salestax) and
			(float) $salestax)
		{
			$xml .= '			<SalesTaxAmount>' . sprintf('%01.2f', (float) $salestax) . '</SalesTaxAmount>' . QUICKBOOKS_CRLF;
		}

		//<BatchID >STRTYPE</BatchID> <!-- optional -->

		if ($comment)
		{
			$xml .= '			<Comment>' . substr(QuickBooks_XML::encode($comment), 0, 500) . '</Comment>' . QUICKBOOKS_CRLF;
		}

		$xml .= '		</CustomerCreditCardTxnVoidOrRefundRq>' . QUICKBOOKS_CRLF;
		$xml .= '	</QBMSXMLMsgsRq>' . QUICKBOOKS_CRLF;
		$xml .= '</QBMSXML>' . QUICKBOOKS_CRLF;

		return $this->_doQBMS(QuickBooks_MerchantService::TYPE_VOIDORREFUND, 'QBMSXML/QBMSXMLMsgsRs/CustomerCreditCardTxnVoidOrRefundRs', $xml);
	}

	protected function _getBatch($BatchID, $for_close = false)
	{
		if (!$BatchID)
		{
			if ($this->_batch)
			{
				return $this->_batch;
			}
			else
			{
				$BatchID = date('md');
			}
		}

		return $BatchID;
	}

	public function batchOpen($BatchID = null)
	{
		$this->_batch = $this->_getBatch($BatchID);
	}

	public function batchClose($BatchID = null)
	{
		$this->_setError(QuickBooks_MerchantService::ERROR_OK);

		if (!$this->isSignedOn())
		{
			$this->signOn();

			if ($this->errorNumber())
			{
				return false;
			}
		}

		$BatchID = $this->_getBatch($BatchID, true);

		// Send the batch close request

	}

	/**
	 * Get the HTTP/HTTPS gateway to use
	 *
	 * @return string
	 */
	protected function _gateway()
	{
		if ($this->_test)
		{
			$this->_log('Using TEST gateway: ' . $this->_test_gateway, QUICKBOOKS_LOG_DEVELOP);
			return $this->_test_gateway;
		}

		$this->_log('Using LIVE gateway: ' . $this->_live_gateway, QUICKBOOKS_LOG_DEVELOP);
		return $this->_live_gateway;
	}

	/**
	 *
	 *
	 *
	 * @param string $message
	 * @param integer $level
	 * @return boolean
	 */
	protected function _log($message, $level = QUICKBOOKS_LOG_NORMAL)
	{
		if ($this->_masking)
		{
			$message = QuickBooks_Utilities::mask($message);
		}

		if ($this->_debug)
		{
			print($message . QUICKBOOKS_CRLF);
		}

		if ($this->_driver)
		{
			// Send it to the driver to be logged
			$this->_driver->log($message, $this->_ticket_session, $level);
		}

		return true;
	}

	/**
	 * Log a message
	 *
	 *
	 */
	public function log($message, $level = QUICKBOOKS_LOG_NORMAL)
	{
		return $this->_log($message, $level);
	}

	/**
	 *
	 *
	 * @param string $xml
	 * @param integer $errnum
	 * @param string $errmsg
	 * @return string
	 */
	protected function _request($xml, &$errnum, &$errmsg)
	{
		$HTTP = new QuickBooks_HTTP($this->_gateway());

		$headers = array(
			'Content-Type' => 'application/x-qbmsxml',
			);
		$HTTP->setHeaders($headers);

		// Turn on debugging for the HTTP object if it's been enabled in the payment processor
		$HTTP->useDebugMode($this->_debug);

		//
		$HTTP->setRawBody($xml);

		if ($this->_certificate)
		{
			$HTTP->setCertificate($this->_certificate);
		}

		$return = $HTTP->POST();

		$this->_last_request = $HTTP->lastRequest();
		$this->_last_response = $HTTP->lastResponse();

		//
		$this->_log($HTTP->getLog(), QUICKBOOKS_LOG_DEBUG);

		$errnum = $HTTP->errorNumber();
		$errmsg = $HTTP->errorMessage();

		if ($errnum)
		{
			// An error occurred!
			$this->_setError(QuickBooks_MerchantService::ERROR_HTTP, $errnum . ': ' . $errmsg);
			return false;
		}

		// Everything is good, return the data!
		$this->_setError(QuickBooks_MerchantService::ERROR_OK, '');
		return $return;
	}
}


<?php

/**
 * QuickBooks singleton class for the queueing class
 *
 * Copyright (c) {2010-04-16} {Keith Palmer / ConsoliBYTE, LLC.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.opensource.org/licenses/eclipse-1.0.php
 *
 * Web Connector applications will often use the queueing class within many
 * different functions within the applications. It is desireable then to always
 * use a single instance of the queueing class to avoid unneccessary database
 * connections and code cruft. This singleton class provides a way to use a
 * single instance of the queueing class without resorting to the use of
 * globals.
 *
 * @author Keith Palmer <keith@consolibyte.com>
 * @license LICENSE.txt
 *
 * @package QuickBooks
 * @subpackage Queue
 */

/**
 * QuickBooks singleton class for the QuickBooks_Queue class
 */
class QuickBooks_WebConnector_Queue_Singleton
{
	/**
	 * Initialize the queueing object
	 *
	 * @param string $dsn
	 * @param string $user
	 * @param array  $config
	 * @param bool   $return_boolean
	 * @return QuickBooks_WebConnector_Queue
	 */
	static public function initialize($dsn = null, $user = null, $config = array(), $return_boolean = true)
	{
		static $instance;
		if (empty($instance))
		{
			if (empty($dsn))
			{
				return false;
			}

			$instance = new QuickBooks_WebConnector_Queue($dsn, $user, $config);
		}

		if ($return_boolean and $instance)
		{
			return true;
		}

		return $instance;
	}

	/**
	 * ???
	 */
	static protected function _hash($dsn, $config)
	{
		return md5(serialize($dsn) . serialize($config));
	}

	/**
	 * Get the instance of the queueing class
	 *
	 * @return QuickBooks_WebConnector_Queue
	 */
	static public function getInstance()
	{
		return QuickBooks_WebConnector_Queue_Singleton::initialize(null, null, null, false);
	}
}


<?php

/**
 * QuickBooks SOAP server for interacting with the QuickBooks Web Connector
 *
 * Copyright (c) 2010-04-16 Keith Palmer / ConsoliBYTE, LLC.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.opensource.org/licenses/eclipse-1.0.php
 *
 * This class provides a framework for generating and handling messages to send
 * to and from QuickBooks via the Web Connector. For every operation you wish
 * to perform on QuickBooks, you should be expecting to implement two functions:
 * 	- A function responsible for generating the qbXML request (telling QuickBooks what you want to do)
 * 	- A function responsible for parsing and handling the qbXML response (handling the response form QuickBooks)
 *
 * You should see the docs/example_server.php file for detailed examples of
 * using this!
 *
 * Add items to the QuickBooks queue using the {@see QuickBooks_Queue} class.
 * The next time the QuickBooks Web Connector connects to the SOAP server, it
 * will be instructed to perform commands based on what has been placed in the
 * queue. So if you queued up "CustomerAdd" "1234", #1234,
 * customer_add_request() will generate a qbXML request telling QuickBooks to
 * add that customer, the SOAP server will send that request out, and
 * QuickBooks will send back a qbXML response indicating whether or not that
 * customer was added successfully. .
 *
 * The QuickBooks Web Connector (QBWC) works like this:
 * 	- You create a SOAP server that response to a set of SOAP methods
 * 	- You install and run the QBWC alongside your existing QuickBooks installation
 * 	- You register your SOAP server with the QBWC
 * 	- The QBWC calls the ->authenticate() method via a SOAP request
 * 	- You create and assign a 'ticket' (essentially a session ID value) to the QBWC session, this ticket gets sent to your SOAP server for authentication purposes with every request thereafter
 * 	- The QBWC calls the ->sendRequestXML() method via a SOAP request, if there is work to do, you send back qbXML commands encapsulated in an object
 * 	- The QBWC passes these qbXML commands to QuickBooks, QuickBooks processes them and passes them back
 * 	- The QBWC passes back the response from QuickBooks to your SOAP server via a SOAP call to ->receiveResponseXML()
 * 	- If you return an integer between 0 and 99 (inclusive) from ->receiveResponseXML(), the QBWC will call ->sendRequestXML() again, to get the next qbXML command
 * 	- Once you return a 100 from ->receiveResponseXML(), the QBWC calls ->closeConnection() and closes the socket connection shortly thereafter
 *
 * Troubleshooting:
 * 	- Errors which QuickBooks reports will be logged (check the quickbooks_queue and quickbooks_log tables in your database)
 * 	- If actions get stuck in the queue and never seem to be pulled out, it is most likely that you're generating badly-formed XML for that request. Check the XML document you're creating for well-formedness, *and especially* character set issues
 * 	- The quickbooks_log database table shows everything that is happening
 *
 * @author Keith Palmer <keith@consolibyte.com>
 * @license LICENSE.txt
 *
 * @package QuickBooks
 * @subpackage Server
 */

/**
 * Hook which gets called when a request is received
 * @var string
 */
define('QUICKBOOKS_SERVER_HOOK_PREHANDLE', 'QuickBooks_Server::handle (pre)');

/**
 * Hook which gets called after a request gets handled
 * @var string
 */
define('QUICKBOOKS_SERVER_HOOK_POSTHANDLE', 'QuickBooks_Server::handle (post)');

/**
 * Base handlers for each of the methods required by the QuickBooks Web Connector
 */
QuickBooks_Loader::load('/QuickBooks/WebConnector/Handlers.php');

/**
 * QuickBooks SOAP Server
 */
class QuickBooks_WebConnector_Server
{
	/**
	 * WSDL location
	 * @var string
	 */
	protected $_wsdl;

	/**
	 * The logging level
	 * @var integer
	 */
	protected $_loglevel;

	/**
	 * Driver instance object
	 * @var object
	 */
	protected $_driver;

	/**
	 * Server instance object
	 * @var object
	 */
	protected $_server;

	/**
	 * Registered hook functions for the server
	 * @var array
	 */
	protected $_hooks;

	/**
	 * An array of data to pass to every callback function
	 * @var array
	 */
	protected $_callback_config;

	/**
	 * The raw input to the script
	 * @var string
	 */
	protected $_input;

	/**
	 * Create a new QuickBooks SOAP server
	 *
	 * @param mixed $dsn_or_conn		Either a DSN-style connection string *or* a database resource (if reusing an existing connection)
	 * @param array $map				An associative array mapping queued commands to function/method calls
	 * @param array $onerror			An associative array mapping error codes to function/method calls
	 * @param array $hooks				An associative array mapping events to hook function/method calls
	 * @param string $wsdl				The path to the WSDL file to use for the SOAP server methods
	 * @param array $soap_options		Options to pass to the SOAP server (these mirror the default PHP SOAP server options)
	 * @param array $handler_options	Options to pass to the handler class
	 * @param array $driver_options		Options to pass to the driver class (i.e.: MySQL, etc.)
	 */
	public function __construct($dsn_or_conn, $map, $onerror = array(), $hooks = array(), $log_level = QUICKBOOKS_LOG_NORMAL, $soap = QUICKBOOKS_SOAPSERVER_BUILTIN, $wsdl = QUICKBOOKS_WSDL, $soap_options = array(), $handler_options = array(), $driver_options = array(), $callback_options = array())
	{
		$soap_options = $this->_defaults($soap_options);

		// If safe mode is turned on, this causes a NOTICE/WARNING to be issued...
		if (!ini_get('safe_mode'))
		{
			set_time_limit($soap_options['time_limit']);
		}

		/*
		if ($soap_options['error_handler'])
		{
			set_error_handler($soap_options['error_handler']);
		}
		else if ($soap_options['use_builtin_error_handler'])
		{
			set_error_handler( array( 'QuickBooks_ErrorHandler', 'handle' ) );
		}

		if ($soap_options['log_to_syslog'])
		{

		}

		if ($soap_options['log_to_file'])
		{

		}
		*/

		// WSDL location
		$this->_wsdl = $wsdl;

		// Logging level
		$this->_loglevel = $log_level;

		if ($this->_loglevel >= QUICKBOOKS_LOG_DEVELOP)
		{
			$this->_driver = QuickBooks_Utilities::driverFactory($dsn_or_conn, $driver_options, $hooks, $log_level);
		}

		// SOAP server adapter class
		$this->_server = $this->_adapterFactory($soap, $wsdl, $soap_options, $log_level);

		/*
		$this->_hooks = array();
		foreach ($hooks as $hook => $funcs)
		{
			if (!is_array($funcs))
			{
				$funcs = array( $funcs );
			}

			$hooks[$hook] = $funcs;			// Do this so that when we pass it to the handlers, the hooks are already in lists
			$this->_hooks[$hook] = $funcs;
		}
		*/

		// Assign hooks
		$this->_hooks = $hooks;

		// Assign callback configuration info
		$this->_callback_config = $callback_options;

		// Raw input
		$input = file_get_contents('php://input');

		$this->_input = $input;

		// Base handlers
		// $dsn_or_conn, $map, $onerror, $hooks, $log_level, $input, $handler_config = array(), $driver_config = array()
		$this->_server->setClass('QuickBooks_WebConnector_Handlers', $dsn_or_conn, $map, $onerror, $hooks, $log_level, $input, $handler_options, $driver_options, $callback_options);
	}

	/**
	 * Get an adapter class instance
	 *
	 * @param string $adapter
	 * @param string $wsdl
	 * @param array $soap_options
	 * @param integer $loglevel
	 * @return boolean
	 */
	protected function _adapterFactory($adapter, $wsdl, $soap_options, $loglevel)
	{
		$adapter = ucfirst(strtolower($adapter));

		$file = '/QuickBooks/Adapter/Server/' . $adapter . '.php';
		$class = 'QuickBooks_Adapter_Server_' . $adapter;

		QuickBooks_Loader::load($file);

		if (class_exists($class))
		{
			return new $class($wsdl, $soap_options);
		}

		return null;
	}

	/**
	 * Merge configurations with the defaults
	 *
	 * @param array $arr
	 * @return array
	 */
	final protected function _defaults($arr)
	{
		$defaults = array(
			'error_handler' => '',
			'use_builtin_error_handler' => false,
			'time_limit' => 0,
			'log_to_file' => null,
			'log_to_syslog' => null,
			'masking' => true,
			);

		$arr = array_merge($defaults, $arr);

		return $arr;
	}

	/**
	 * Merge two arrays, allowing $arr2 to be merged over matching keys in $arr1
	 *
	 * If $arr1 or $arr2 are arrays of arrays, and $array_of_arrays is set to
	 * true, then the arrays of arrays will be merged, allowing $arr2 to
	 * override $arr1 entries. If the arrays of arrays are numerically indexed,
	 * $arr2 entries will be appended to $arr1 entries.
	 *
	 * @param array $arr1
	 * @param array $arr2
	 * @param boolean $array_of_arrays
	 * @return array
	 */
	protected function _merge($arr1, $arr2, $array_of_arrays = false)
	{
		if ($array_of_arrays)
		{
			foreach ($arr2 as $key => $funcs)
			{
				if (!is_array($funcs))
				{
					$funcs = array( $funcs );
				}

				if (isset($arr1[$key]))
				{
					if (!is_array($arr1[$key]))
					{
						$arr1[$key] = array( $arr1[$key] );
					}

					$arr1[$key] = array_merge($arr1[$key], $funcs);
				}
				else
				{
					$arr1[$key] = $funcs;
				}
			}

			return $arr1;
		}
		else
		{
			// *DO NOT* use array_merge() here, it screws things up!!!
			//return array_merge($arr1, $arr2);

			foreach ($arr2 as $key => $value)
			{
				$arr1[$key] = $value;
			}

			return $arr1;
		}
	}

	/**
	 * Send the correct HTTP headers for this request
	 *
	 * @return boolean
	 */
	protected function _headers()
	{
		if ($_SERVER['REQUEST_METHOD'] == 'POST')
		{
			header('Content-Type: text/xml');
		}
		else if (isset($_GET['wsdl']) or isset($_GET['WSDL']))
		{
			header('Content-Type: text/xml');
		}
		else
		{
			header('Content-Type: text/plain');
		}

		return true;
	}

	/**
	 * Log a message to the error/debug log
	 *
	 * @param string $msg
	 * @param string $ticket
	 * @param integer $level
	 * @return boolean
	 */
	protected function _log($msg, $ticket, $level = QUICKBOOKS_LOG_NORMAL)
	{
		$Driver = $this->_driver;

		$msg = QuickBooks_Utilities::mask($msg);

		if ($Driver)
		{
			return $Driver->log($msg, $ticket, $level);
		}

		return false;
	}

	/**
	 * Handle the SOAP request
	 *
	 * @param boolean $return
	 * @param boolean $debug
	 * @return void
	 */
	public function handle($return = false, $debug = false)
	{
		// Get the raw input
		$input = $this->_input;

		//
		if ($_SERVER['REQUEST_METHOD'] == 'POST')
		{
			$this->_headers();

			$output_buffering = false;

			/*
			if (isset($this->_hooks[QUICKBOOKS_SERVER_HOOK_PREHANDLE]))
			{
				foreach ($this->_hooks[QUICKBOOKS_SERVER_HOOK_PREHANDLE] as $func)
				{
					$func($input, $this->_callback_config);
				}
			}
			*/

			$hook_data = array(
				'input' => $input,
				);

			$err = '';
			$this->_callHooks(QUICKBOOKS_SERVER_HOOK_PREHANDLE, null, null, null, $err, $hook_data);
			//QuickBooks_Callbacks::callHook($this->_driver, $this->_hooks, QUICKBOOKS_SERVER_HOOK_PREHANDLE, null, null, null, $err, $hook_data, $this->_callback_config, __FILE__, __LINE__);

			if ($this->_loglevel >= QUICKBOOKS_LOG_DEVELOP)
			{
				if (function_exists('apache_request_headers'))
				{
					$headers = '';
					foreach (apache_request_headers() as $header => $value)
					{
						$headers .= $header . ': ' . $value . "\n";
					}

					//$this->_driver->log('Incoming HTTP Headers: ' . $headers, null, QUICKBOOKS_LOG_DEVELOP);
					$this->_log('Incoming HTTP Headers: ' . $headers, null, QUICKBOOKS_LOG_DEVELOP);
				}

				//$this->_driver->log('Incoming SOAP Request: ' . $input, null, QUICKBOOKS_LOG_DEVELOP);
				$this->_log('Incoming SOAP Request: ' . $input, null, QUICKBOOKS_LOG_DEVELOP);
			}

			if ($return or isset($this->_hooks[QUICKBOOKS_SERVER_HOOK_POSTHANDLE]))
			{
				$output_buffering = true;
				ob_start();
			}

			$this->_server->handle($input);

			if ($return or
				isset($this->_hooks[QUICKBOOKS_SERVER_HOOK_POSTHANDLE]) or
				$this->_loglevel >= QUICKBOOKS_LOG_DEVELOP)
			{
				$output = '';
				if ($output_buffering)
				{
					$output = ob_get_contents();
					ob_end_flush();
				}

				/*
				if (isset($this->_hooks[QUICKBOOKS_SERVER_HOOK_POSTHANDLE]))
				{
					foreach ($this->_hooks[QUICKBOOKS_SERVER_HOOK_POSTHANDLE] as $func)
					{
						$func($output, $this->_callback_config);
					}
				}
				*/

				$hook_data = array(
					'input' => $input,
					'output' => $output,
					);

				$err = '';
				$this->_callHooks(QUICKBOOKS_SERVER_HOOK_POSTHANDLE, null, null, null, $err, $hook_data);
				//QuickBooks_Callbacks::callHook($this->_driver, $this->_hooks, QUICKBOOKS_SERVER_HOOK_POSTHANDLE, null, null, null, $err, $hook_data, $this->_callback_config);

				if ($this->_loglevel >= QUICKBOOKS_LOG_DEVELOP)
				{
					//$this->_driver->log('Outgoing SOAP Response: ' . $output, null, QUICKBOOKS_LOG_DEVELOP);
					$this->_log('Outgoing SOAP Response: ' . $output, null, QUICKBOOKS_LOG_DEVELOP);
				}

				if ($return)
				{
					return $output;
				}
			}

			return;
		}
		else if (isset($_GET['WSDL']) or isset($_GET['wsdl']))
		{
			if ($contents = file_get_contents($this->_wsdl))
			{
				$this->_headers();
				print($contents);
				exit;
			}
		}
		else
		{
			$this->_headers();

			print(QUICKBOOKS_PACKAGE_NAME . ' Server v' . QUICKBOOKS_PACKAGE_VERSION . ' at ' . $_SERVER['REQUEST_URI'] . "\n");
			print('   (c) ' . QUICKBOOKS_PACKAGE_AUTHOR . ' ' . "\n");
			print('   Visit us at: ' . QUICKBOOKS_PACKAGE_WEBSITE . ' ' . "\n");
			print("\n");
			print('Use the QuickBooks Web Connector to access this SOAP server.' . "\n");
			print("\n");

			if ($debug)
			{
				print(get_class($this) . str_replace(__CLASS__, '', __METHOD__) . '() parameters: ' . "\n");
				print(' - $return = ' . $return . "\n");
				print(' - $debug  = ' . $debug . "\n");
				print("\n");
				print('Misc. information: ' . "\n");
				print(' - Logging: ' . $this->_loglevel . "\n");

				if (function_exists('date_default_timezone_get'))
				{
					print(' - Timezone: ' . date_default_timezone_get() . ' (Auto-set: ');

					if (QUICKBOOKS_TIMEZONE_AUTOSET)
					{
						print('Yes');
					}

					print(')' . "\n");
				}
				print(' - Current Date/Time: ' . date('Y-m-d H:i:s') . "\n");
				print(' - Error Reporting: ' . error_reporting() . "\n");

				print("\n");
				print('SOAP adapter: ' . "\n");
				print(' - ' . get_class($this->_server) . "\n");
				print("\n");
				print('Registered handler functions: ' . "\n");
				print_r($this->_server->getFunctions());

				/*
				print("\n");
				print('Registered hooks: ' . "\n");
				//print_r($this->_hooks);		// This is bad because it prints passwords
				foreach ($this->_hooks as $hook => $arr)
				{
					if (!is_array($arr))
					{
						continue;
					}

					print(' - ' . $hook . QUICKBOOKS_CRLF);
					foreach ($arr as $x)
					{
						$y = current(explode("\n", print_r($x, true)));

						print('    ' . $y . QUICKBOOKS_CRLF);
					}
				}
				*/

				print("\n");
				print('Detected input: ' . "\n");
				print($input);
				print("\n");
				print("\n");
				print('Timestamp: ' . "\n");
				print(' - ' . date('Y-m-d H:i:s') . ' -- process ' . round(microtime(true) - QUICKBOOKS_TIMESTAMP, 5) . "\n");
			}

			return;
		}
	}

	/**
	 *
	 *
	 * @param string $hook
	 * @param string $requestID
	 * @param string $user
	 * @param string $ticket
	 * @param string $err
	 * @param array $hook_data
	 * @return boolean
	 */
	protected function _callHooks($hook, $requestID, $user, $ticket, &$err, $hook_data)
	{
		$err = '';
		return QuickBooks_Callbacks::callHook(
			$this->_driver,
			$this->_hooks,
			$hook,
			$requestID,
			$user,
			$ticket,
			$err,
			$hook_data,
			$this->_callback_config);
	}

	/**
	 * Get debugging information from the SOAP server
	 *
	 * @return array
	 */
	public function debug()
	{
		return var_export($this, true);
	}
}


<?php
/**
 * JournalEntry class for QuickBooks
 *
 * @author Keith Palmer Jr. <keith@ConsoliBYTE.com>
 * @license LICENSE.txt
 *
 * @package QuickBooks
 * @subpackage Object
 */

/**
 * QuickBooks object base class
 */
QuickBooks_Loader::load('/QuickBooks/QBXML/Object.php');

/**
 *
 */
class QuickBooks_QBXML_Object_JournalEntry_JournalCreditLine extends QuickBooks_QBXML_Object
{
	/**
	 * Create a new QuickBooks_Object_JournalEntry object
	 *
	 * @param array $arr
	 */
	public function __construct($arr = array())
	{
		parent::__construct($arr);
	}

	// Path: TxnLineID, datatype:

	/**
	 * Set the TxnLineID for the JournalEntry
	 *
	 * @param string $value
	 * @return boolean
	 */
	public function setTxnLineID($value)
	{
		return $this->set('TxnLineID', $value);
	}

	/**
	 * Get the TxnLineID for the JournalEntry
	 *
	 * @return string
	 */
	public function getTxnLineID()
	{
		return $this->get('TxnLineID');
	}

	// Path: AccountRef ListID, datatype:

	/**
	 * Set the AccountRef ListID for the JournalEntry
	 *
	 * @param string $ListID		The ListID of the record to reference
	 * @return boolean
	 */
	public function setAccountListID($ListID)
	{
		return $this->set('AccountRef ListID', $ListID);
	}

	/**
	 * Get the AccountRef ListID for the JournalEntry
	 *
	 * @return string
	 */
	public function getAccountListID()
	{
		return $this->get('AccountRef ListID');
	}

	/**
	 * Set the primary key for the related record within your own application for the JournalEntry
	 *
	 * @param mixed $value			The primary key within your own application
	 * @return string
	 */
	public function setAccountApplicationID($value)
	{
		return $this->set('AccountRef ' . QUICKBOOKS_API_APPLICATIONID, $this->encodeApplicationID(QUICKBOOKS_OBJECT_ACCOUNT, QUICKBOOKS_LISTID, $value));
	}

	// Path: AccountRef FullName, datatype:

	/**
	 * Set the AccountRef FullName for the JournalEntry
	 *
	 * @param string $FullName		The FullName of the record to reference
	 * @return boolean
	 */
	public function setAccountName($FullName)
	{
		return $this->set('AccountRef FullName', $FullName);
	}

	/**
	 * Get the AccountRef FullName for the JournalEntry
	 *
	 * @return string
	 */
	public function getAccountName()
	{
		return $this->get('AccountRef FullName');
	}

	// Path: Amount, datatype:

	/**
	 * Set the Amount for the JournalEntry
	 *
	 * @param float $value
	 * @return boolean
	 */
	public function setAmount($value)
	{
		return $this->setAmountType('Amount', $value);
	}

	/**
	 * Get the Amount for the JournalEntry
	 *
	 * @return float
	 */
	public function getAmount()
	{
		return $this->getAmountType('Amount');
	}

	// Path: Memo, datatype: STRTYPE

	/**
	 * Set the Memo for the JournalEntry
	 *
	 * @param string $value
	 * @return boolean
	 */
	public function setMemo($value)
	{
		return $this->set('Memo', $value);
	}

	/**
	 * Get the Memo for the JournalEntry
	 *
	 * @return string
	 */
	public function getMemo()
	{
		return $this->get('Memo');
	}

	// Path: EntityRef ListID, datatype: STRTYPE

	/**
	 * Set the EntityRef ListID for the JournalEntry
	 *
	 * @param string $ListID		The ListID of the record to reference
	 * @return boolean
	 */
	public function setEntityListID($ListID)
	{
		return $this->set('EntityRef ListID', $ListID);
	}

	/**
	 * Get the EntityRef ListID for the JournalEntry
	 *
	 * @return string
	 */
	public function getEntityListID()
	{
		return $this->get('EntityRef ListID');
	}

	/**
	 * Set the primary key for the related record within your own application for the JournalEntry
	 *
	 * @param mixed $value			The primary key within your own application
	 * @return string
	 */
	public function setEntityApplicationID($value)
	{
		return $this->set('EntityRef ' . QUICKBOOKS_API_APPLICATIONID, $this->encodeApplicationID(QUICKBOOKS_OBJECT_ENTITY, QUICKBOOKS_LISTID, $value));
	}

	// Path: EntityRef FullName, datatype: STRTYPE

	/**
	 * Set the EntityRef FullName for the JournalEntry
	 *
	 * @param string $FullName		The FullName of the record to reference
	 * @return boolean
	 */
	public function setEntityName($FullName)
	{
		return $this->set('EntityRef FullName', $FullName);
	}

	/**
	 * Get the EntityRef FullName for the JournalEntry
	 *
	 * @return string
	 */
	public function getEntityName()
	{
		return $this->get('EntityRef FullName');
	}

	// Path: ClassRef ListID, datatype: STRTYPE

	/**
	 * Set the ClassRef ListID for the JournalEntry
	 *
	 * @param string $ListID		The ListID of the record to reference
	 * @return boolean
	 */
	public function setClassListID($ListID)
	{
		return $this->set('ClassRef ListID', $ListID);
	}

	/**
	 * Get the ClassRef ListID for the JournalEntry
	 *
	 * @return string
	 */
	public function getClassListID()
	{
		return $this->get('ClassRef ListID');
	}

	/**
	 * Set the primary key for the related record within your own application for the JournalEntry
	 *
	 * @param mixed $value			The primary key within your own application
	 * @return string
	 */
	public function setClassApplicationID($value)
	{
		return $this->set('ClassRef ' . QUICKBOOKS_API_APPLICATIONID, $this->encodeApplicationID(QUICKBOOKS_OBJECT_CLASS, QUICKBOOKS_LISTID, $value));
	}

	// Path: ClassRef FullName, datatype: STRTYPE

	/**
	 * Set the ClassRef FullName for the JournalEntry
	 *
	 * @param string $FullName		The FullName of the record to reference
	 * @return boolean
	 */
	public function setClassName($FullName)
	{
		return $this->set('ClassRef FullName', $FullName);
	}

	/**
	 * Get the ClassRef FullName for the JournalEntry
	 *
	 * @return string
	 */
	public function getClassName()
	{
		return $this->get('ClassRef FullName');
	}

	// Path: ItemSalesTaxRef ListID, datatype: STRTYPE

	/**
	 * Set the ItemSalesTaxRef ListID for the JournalEntry
	 *
	 * @param string $ListID		The ListID of the record to reference
	 * @return boolean
	 */
	public function setItemSalesTaxListID($ListID)
	{
		return $this->set('ItemSalesTaxRef ListID', $ListID);
	}

	/**
	 * Get the ItemSalesTaxRef ListID for the JournalEntry
	 *
	 * @return string
	 */
	public function getItemSalesTaxListID()
	{
		return $this->get('ItemSalesTaxRef ListID');
	}

	/**
	 * Set the primary key for the related record within your own application for the JournalEntry
	 *
	 * @param mixed $value			The primary key within your own application
	 * @return string
	 */
	public function setItemSalesTaxApplicationID($value)
	{
		return $this->set('ItemSalesTaxRef ' . QUICKBOOKS_API_APPLICATIONID, $this->encodeApplicationID(QUICKBOOKS_OBJECT_ITEMSALESTAX, QUICKBOOKS_LISTID, $value));
	}

	// Path: ItemSalesTaxRef FullName, datatype: STRTYPE

	/**
	 * Set the ItemSalesTaxRef FullName for the JournalEntry
	 *
	 * @param string $FullName		The FullName of the record to reference
	 * @return boolean
	 */
	public function setItemSalesTaxName($FullName)
	{
		return $this->set('ItemSalesTaxRef FullName', $FullName);
	}

	/**
	 * Get the ItemSalesTaxRef FullName for the JournalEntry
	 *
	 * @return string
	 */
	public function getItemSalesTaxName()
	{
		return $this->get('ItemSalesTaxRef FullName');
	}

	// Path: BillableStatus, datatype:

	/**
	 * Set the BillableStatus for the JournalEntry
	 *
	 * @param string $value
	 * @return boolean
	 */
	public function setBillableStatus($value)
	{
		return $this->set('BillableStatus', $value);
	}

	/**
	 * Get the BillableStatus for the JournalEntry
	 *
	 * @return string
	 */
	public function getBillableStatus()
	{
		return $this->get('BillableStatus');
	}

	/**
	 * Tell the type of object this is
	 *
	 * @return string
	 */
	public function object()
	{
		return 'JournalCreditLine';
	}
}


<?php

/**
 *
 *
 * Copyright (c) 2010 Keith Palmer / ConsoliBYTE, LLC.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.opensource.org/licenses/eclipse-1.0.php
 *
 * @author Keith Palmer <keith@consolibyte.com>
 * @license LICENSE.txt
 *
 * @package QuickBooks
 * @subpackage Server
 */

/**
 *
 */
class QuickBooks_Callbacks_Integrator_Errors
{
	/**
	 *
	 * Error message:
	 * 	"3170: There was an error when modifying a data extension named
	 * 	"e-mail".  QuickBooks error message: This list has been
	 * 	modified by another user."
	 *
	 * Solution:
	 * 	Send the request again, it usually goes through the second time.
	 *
	 *
	 */
	static public function e3170_errorsaving($requestID, $user, $action, $ID, $extra, &$err, $xml, $errnum, $errmsg, $config)
	{
		switch ($action)
		{
			case QUICKBOOKS_MOD_DATAEXT:

				// Ignore it for now... oops!
				return true;
			default:
				return false;
		}

		// (default clause for switch() statement)
	}

	/**
	 *
	 *
	 *
	 */
	static public function e3180_errorsaving($requestID, $user, $action, $ID, $extra, &$err, $xml, $errnum, $errmsg, $config)
	{
		switch ($action)
		{
			case QUICKBOOKS_ADD_DATAEXT:

				// Ignore it, this happens when we try to DataExtAdd a DataExt that already exists
				return true;
			default:
				return false;
		}

		// (default clause for switch() statement)
	}

	static public function e3200_editsequence($requestID, $user, $action, $ID, $extra, &$err, $xml, $errnum, $errmsg, $config)
	{
		switch ($action)
		{
			case QUICKBOOKS_MOD_CUSTOMER:

				// EditSequence for this customer is out-of-date, query for the customer to get the latest EditSequence, and re-send
				return QuickBooks_Callbacks_Integrator_Callbacks::integrateQueryCustomer($ID);
			default:
				return false;
		}
	}

	/**
	 *
	 *
	 *
	 *
	 */
	static public function e3100_alreadyexists($requestID, $user, $action, $ID, $extra, &$err, $xml, $errnum, $errmsg, $config)
	{
		//print('exists!');

		// These are special-case handlers, handle these by querying
		switch ($ID)
		{
			case QUICKBOOKS_INTEGRATOR_COUPON_ID:
			case QUICKBOOKS_INTEGRATOR_SHIPPING_ID:
			case QUICKBOOKS_INTEGRATOR_HANDLING_ID:
			case QUICKBOOKS_INTEGRATOR_DISCOUNT_ID:

				// @TODO Fix this... I'm not sure whether we should issue another
				//	query (havn't we queried already...?) or just ignore this because
				//	we'll refer to these items by FullName in the requests, so if
				//	it already exists we're golden, or...?

				return true;

				switch ($action)
				{
					case QUICKBOOKS_ADD_SERVICEITEM:

						break;
					case QUICKBOOKS_ADD_DISCOUNTITEM:

						break;
					case QUICKBOOKS_ADD_OTHERCHARGEITEM:

						break;
				}

				return true;
		}

		switch ($action)
		{
			case QUICKBOOKS_ADD_PAYMENTMETHOD:

				break;
			case QUICKBOOKS_ADD_SHIPMETHOD:

				break;
			case QUICKBOOKS_ADD_NONINVENTORYITEM:

				break;
			case QUICKBOOKS_ADD_INVENTORYITEM:

				break;
			case QUICKBOOKS_ADD_SERVICEITEM:

				return true;
			case QUICKBOOKS_ADD_CUSTOMER:

				// Do a query for the customer
				// @todo Does this have the potential to cause an infinite loop?  Add, taken by Vendor, Query, Add, taken by Vendor, Query, etc. etc. etc.
				//return QuickBooks_Callbacks_Integrator_Callbacks::integrateQueryCustomer($ID);
				return true;
		}

		return false;
	}

	/**
	 * This error occurs when we send a request to QuickBooks that has an error, or that QuickBooks doesn't understand (old version of QuickBooks)
	 *
	 * @param string $requestID
	 * @return boolean
	 */
	static public function e0x80040400_foundanerror($requestID, $user, $action, $ident, $extra, &$err, $xml, $errnum, $errmsg, $config)
	{
		if ($action == QUICKBOOKS_QUERY_UNITOFMEASURESET)
		{
			// Some versions don't support this query, so ignore this error
			return true;
		}

		return false;
	}

	static public function e3250_featurenotenabled($requestID, $user, $action, $ident, $extra, &$err, $xml, $errnum, $errmsg, $config)
	{
		if ($action == QUICKBOOKS_QUERY_UNITOFMEASURESET)
		{
			// Some versions don't support UnitOfMeasureSetQuery
			return true;
		}

		return false;
	}

	/**
	 *
	 *
	 *
	 */
	static public function e_catchall($requestID, $user, $action, $ident, $extra, &$err, $xml, $errnum, $errmsg, $config)
	{
		if (!empty($config['_error_email']))
		{
			$msg = '';
			$msg .= 'Error number: ' . $errnum . "\r\n";
			$msg .= 'Error message: ' . $errmsg . "\r\n";
			$msg .= "\r\n";
			$msg .= 'Action: ' . $action . "\r\n";
			$msg .= 'Ident: ' . $ident . "\r\n";
			$msg .= "\r\n";
			$msg .= 'Extra: ' . print_r($extra, true) . "\r\n";
			$msg .= "\r\n";
			$msg .= 'qbXML: ' . $xml . "\r\n";

			mail($config['_error_email'], $config['_error_subject'], $msg, 'From: ' . $config['_error_from'] . "\r\n");
		}

		return false;
	}
}


<?php

/**
 * QuickBooks encryption library base class
 *
 * Copyright (c) 2010 Keith Palmer / ConsoliBYTE, LLC.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.opensource.org/licenses/eclipse-1.0.php
 *
 * @author Keith Palmer <keith@consolibyte.com>
 * @license LICENSE.txt
 *
 * @package QuickBooks
 */

/**
 *
 */
QuickBooks_Loader::load('/QuickBooks/Encryption/Factory.php');

/**
 *
 *
 */
abstract class QuickBooks_Encryption
{
	/**
	 *
	 *
	 *
	 */
	public function prefix($str)
	{
		return '{' . strlen(get_class($this)) . ':' . strtolower(get_class($this)) . '}' . $str;
	}

	/**
	 * AES encryption
	 *
	 * @param string $key
	 * @param string $data		Content to be encrypted
	 * @param bool $hex
	 * @return string
	 */
	static function salt()
	{
		$tmp = array_merge(range('a', 'z'), range('A', 'Z'), range(0, 9));
		shuffle($tmp);

		$salt = substr(implode('', $tmp), 0, 32);

		return $salt;
	}
}


<?php defined('SYSPATH') or die('No direct script access.');

/**
 * QuickBooks Kohana Integration Example
 *
 * This is a sample to get you started using Kohana with the QB framework
 * you may consider moving some methods into objects to help keep the code
 * from growing too large, as it can get very bulky in the framework!
 *
 * See model folder for example of how to move methods out of this
 *
 * @author Jayson Lindsley <jay.lindsley@gmail.com>
 *
 */

//set this to the framework directory
require_once 'framework/QuickBooks.php';

class Controller_Qbapi_Quickbooks extends Controller {

	/**
	 * Prep the page for the web connector
	 * @author Jayson Lindsley <Jayson@nerds4u.net>
	 */
	public function before() {
		// Security checking goes in here

		//Turn on error reporting
		error_reporting(E_ALL | E_STRICT);
		//set the content type
	 	$this->response->headers('Content-Type', 'text/xml');
	 	//set the timezone
		if (function_exists('date_default_timezone_set'))
			date_default_timezone_set('America/Los_Angeles');
	}

	/**
	 * Houses the instance of the soap server and creates the mappings for errors, function callbacks
	 * @author Jayson Lindsley <jay.lindsley@gmail.com>
	 */
	public function action_index()	{
		//Username and password used for the web connector, QWC file, and the QB Framework
		$user = 'QBAPI Username';
		$pass = 'QBAPI Password';

		//Configure the logging level
		$log_level = QUICKBOOKS_LOG_DEVELOP;

		//Pure-PHP SOAP server
		$soapserver = QUICKBOOKS_SOAPSERVER_BUILTIN;

		//we can turn this off
		$handler_options = array(
			'deny_concurrent_logins' => false,
			'deny_reallyfast_logins' => false,
		);

		// The next three params $map, $errmap, and $hooks are callbacks which
		// will be called when certain actions/events/requests/responses occur within
		// the framework

		// Maps inbound requests to functions
		$map = array(
			QUICKBOOKS_ADD_CUSTOMER => array( array($this, '_quickbooks_customer_add_request'), array($this,'_quickbooks_customer_add_response' )),
			QUICKBOOKS_MOD_CUSTOMER => array( array($this, '_quickbooks_customer_mod_request'), array($this, '_quickbooks_customer_mod_response')),
		);

		//Map error handling to functions
		$errmap = array(
			3070 => array($this, '_quickbooks_error_stringtoolong'),
			3140	=> array($this, '_quickbooks_reference_error'),
			'*'	=>	array($this, '_quickbooks_error_handler'),
		);

		//Login success callback
		$hooks = array(
			QuickBooks_WebConnector_Handlers::HOOK_LOGINSUCCESS => array(array($this,'_quickbooks_hook_loginsuccess')),
			);

		//MySQL database name containing the QuickBooks tables is named 'quickbooks' (if the tables don't exist, they'll be created for you)
		$dsn = 'mysql://username:password@host/databasename';

		QuickBooks_WebConnector_Queue_Singleton::initialize($dsn);

		if (!QuickBooks_Utilities::initialized($dsn)) {
			// Initialize creates the neccessary database schema for queueing up requests and logging
			QuickBooks_Utilities::initialize($dsn);

			// This creates a username and password which is used by the Web Connector to authenticate
			QuickBooks_Utilities::createUser($dsn, $user, $pass);

			// Initial test case customer
			$primary_key_of_new_customer = 512;

			// Fire up the Queue
			$Queue = new QuickBooks_WebConnector_Queue($dsn);

			// Drop the directive and the customer into the queue
			$Queue->enqueue(QUICKBOOKS_ADD_CUSTOMER, $primary_key_of_new_customer);

			// Also note the that ->enqueue() method supports some other parameters:
			// 	string $action				The type of action to queue up
			//	mixed $ident = null			Pass in the unique primary key of your record here, so you can pull the data from your application to build a qbXML request in your request handler
			//	$priority = 0				You can assign priorities to requests, higher priorities get run first
			//	$extra = null				Any extra data you want to pass to the request/response handler
			//	$user = null				If you're using multiple usernames, you can pass the username of the user to queue this up for here
			//	$qbxml = null
			//	$replace = true
		}
		//To be used with singleton queue
		$driver_options = array();

		//Callback options, not needed at the moment
		$callback_options = array();

		//nothing needed here at the moment
		$soap_options = array();

		//construct a new instance of the web connector server
		$Server = new QuickBooks_WebConnector_Server($dsn, $map, $errmap, $hooks, $log_level, $soapserver, QUICKBOOKS_WSDL, $soap_options, $handler_options, $driver_options, $callback_options);

		//instruct server to handle responses
		$response = $Server->handle(true, true);
	}

	function _quickbooks_hook_loginsuccess($requestID, $user, $hook, &$err, $hook_data, $callback_config)
	{
		Kohana::$log->add(Log::NOTICE, "Quickbooks Successfully Logged In");

	}

	function _quickbooks_customer_add_request($requestID, $user, $action, $ID, $extra, &$err, $last_action_time, $last_actionident_time, $version, $locale)
	{
		//Fetch the customer using ORM with the unique ID
		$mycustomer = ORM::factory('customer', $ID);
		//Fetch all your customer details using the ID passed back up to this function
		$firstname = $mycustomer->firstname;
		$lastname = $mycustomer->lastname;
		$fullname = $firstname . ' ' . $lastname;
		//Generate a QBXML object
		$Customer = new QuickBooks_QBXML_Object_Customer();
		$Customer->setFullName($fullname);
		$Customer->setFirstName($firstname);
		$Customer->setLastName($lastname);
		$Customer->setCompanyName($companyname);

		//Format the proper QBXML to return to the web connector
		$qbxml = $Customer->asQBXML('CustomerAddRq');
		$qbxml = $this->formatForOutput($qbxml);

		//Log that the customer has been generated
		Kohana::$log->add(Log::NOTICE, "\nCustomer QBXML:\n" . $qbxml);
		//send down to web connector
		return $qbxml;
	}

	function _quickbooks_customer_add_response($requestID, $user, $action, $ID, $extra, &$err, $last_action_time, $last_actionident_time, $xml, $idents)
	{
		Kohana::$log->add(Log::NOTICE, "Add Customer Response: " . $xml);

		Kohana::$log->add(Log::NOTICE, "Applying QB_LISTID to customer: " . $ID . ', listid: ' . $idents['ListID']);

		//fetch the customer
		$mycustomer = ORM::factory('customer', $ID);
		//Save the list id
		$customerdata->listid = $idents['ListID'];
		//save an edit sequence
		$customerdata->editsequence = $idents['EditSequence'];
		//mark the customer as success
		$customerdata->status = 0;
		//save using ORM
		$customerdata->save();
	}

	/* Customer Modification Request Block */
	function _quickbooks_customer_mod_request($requestID, $user, $action, $ID, $extra, &$err, $last_action_time, $last_actionident_time, $version, $locale)
	{
		Kohana::$log->add(Log::NOTICE, "\nCustomer Modification QBXML:\n" . $qbxml);
	}

	function _quickbooks_customer_mod_response($requestID, $user, $action, $ID, $extra, &$err, $last_action_time, $last_actionident_time, $xml, $idents)
	{
		Kohana::$log->add(Log::NOTICE, "Mod Customer Response: " . $xml);
	}

	private function formatForOutput($string)
	{
		$return = '<?xml version="1.0" encoding="utf-8"?>
			<?qbxml version="2.0"?>
			<QBXML>
				<QBXMLMsgsRq onError="continueOnError">
				' . $string . '
				</QBXMLMsgsRq>
				</QBXML>';
		return $return;
	}

	 function _quickbooks_error_stringtoolong($requestID, $user, $action, $ID, $extra, &$err, $xml, $errnum, $errmsg)
	{

	}

	function _quickbooks_reference_error($requestID, $user, $action, $ID, $extra, &$err, $xml, $errnum, $errmsg)
	{
		//log the error
		Kohana::$log->add(Log::ERROR, "Reference Error : " . $errmsg);
	}

	function _quickbooks_error_handler($requestID, $user, $action, $ID, $extra, &$err, $xml, $errnum, $errmsg)
	{
		Kohana::$log->add(Log::ERROR, "New Error: " . $requestID . ", action: " . $action . ", ID: " . $ID . "\nmessage:\n" .$errmsg . " - number: " . $errnum);
	}
}