<?php
/**
* @version	$Id: google_checkout.php 13465 2010-04-28 15:08:00Z alex $
* @package	In-Commerce
* @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license	Commercial License
* This software is protected by copyright law and international treaties.
* Unauthorized reproduction or unlicensed usage of the code of this program,
* or any portion of it may result in severe civil and criminal penalties,
* and will be prosecuted to the maximum extent possible under the law
* See http://www.in-portal.org/commercial-license for copyright notices and details.
*/

	require_once GW_CLASS_PATH.'/gw_base.php';

	$class_name = 'kGWGoogleCheckout'; // for automatic installation

	class kGWGoogleCheckout extends kGWBase
	{
		var $gwParams = Array ();

		function InstallData()
		{
			$data = array(
				'Gateway' => Array('Name' => 'Google Checkout', 'ClassName' => 'kGWGoogleCheckout', 'ClassFile' => 'google_checkout.php', 'RequireCCFields' => 0),
				'ConfigFields' => Array(
					'submit_url' => Array('Name' => 'Submit URL', 'Type' => 'text', 'ValueList' => '', 'Default' => 'https://checkout.google.com/api/checkout/v2'),
					'merchant_id' => Array('Name' => 'Google merchant ID', 'Type' => 'text', 'ValueList' => '', 'Default' => ''),
					'merchant_key' => Array('Name' => 'Google merchant key', 'Type' => 'text', 'ValueList' => '', 'Default' => ''),
					'shipping_control' => Array('Name' => 'Shipping Control', 'Type' => 'select', 'ValueList' => '3=la_CreditDirect,4=la_CreditPreAuthorize', 'Default' => 3),
				)
			);
			return $data;
		}

		/**
		 * Returns payment form submit url
		 *
		 * @param Array $gw_params gateway params from payment type config
		 * @return string
		 */
		function getFormAction($gw_params)
		{
			return $gw_params['submit_url'].'/checkout/Merchant/'.$gw_params['merchant_id'];
		}

		/**
		 * Processed input data and convets it to fields understandable by gateway
		 *
		 * @param Array $item_data current order fields
		 * @param Array $tag_params additional params for gateway passed through tag
		 * @param Array $gw_params gateway params from payment type config
		 * @return Array
		 */
		function getHiddenFields($item_data, $tag_params, $gw_params)
		{
			$ret = Array();
			$this->gwParams = $gw_params;

			$cart_xml = $this->getCartXML($item_data);

			$ret['cart'] = base64_encode($cart_xml);
    		$ret['signature'] = base64_encode( $this->CalcHmacSha1($cart_xml, $gw_params) );

			return $ret;
		}

		function getCartXML($cart_fields)
		{
			// 1. prepare shopping cart content
			$sql = 'SELECT *
					FROM '.TABLE_PREFIX.'OrderItems oi
					LEFT JOIN '.TABLE_PREFIX.'Products p ON p.ProductId = oi.ProductId
					WHERE oi.OrderId = '.$cart_fields['OrderId'];
			$order_items = $this->Conn->Query($sql);

			$ml_formatter =& $this->Application->recallObject('kMultiLanguage');
			/* @var $ml_formatter kMultiLanguage */

			$cart_xml = Array ();
			foreach ($order_items as $order_item) {
				$cart_xml[] = '	<item>
					        		<item-name>'.htmlspecialchars($order_item['ProductName']).'</item-name>
					        		<item-description>'.htmlspecialchars($order_item[$ml_formatter->LangFieldName('DescriptionExcerpt')]).'</item-description>'.
									$this->getPriceXML('unit-price', $order_item['Price']).'
					        		<quantity>'.$order_item['Quantity'].'</quantity>
								</item>';
			}
			$cart_xml = '<items>'.implode("\n", $cart_xml).'</items>';

			// 2. add order identification info (for google checkout notification)
			$cart_xml .= '	<merchant-private-data>
								<session_id>'.$this->Application->GetSID().'</session_id>
								<order_id>'.$cart_fields['OrderId'].'</order_id>
							</merchant-private-data>';

			// 3. add all shipping types (with no costs)
			$sql = 'SELECT Name
					FROM '.TABLE_PREFIX.'ShippingType
					WHERE Status = '.STATUS_ACTIVE;
			$shipping_types = $this->Conn->GetCol($sql);

			$shipping_xml = '';
			foreach ($shipping_types as $shipping_name) {
				$shipping_xml .= '	<merchant-calculated-shipping name="'.htmlspecialchars($shipping_name).'">
										<price currency="USD">0.00</price>
									</merchant-calculated-shipping>';
			}

			$use_ssl = substr($this->gwParams['submit_url'], 0, 8) == 'https://' ? true : null;
			$shipping_url = $this->Application->BaseURL('/in-commerce/units/gateways/gw_classes/notify_scripts', $use_ssl).'google_checkout_shippings.php';

			$shipping_xml = '<merchant-checkout-flow-support>
								<shipping-methods>'.$shipping_xml.'</shipping-methods>
					      		<merchant-calculations>
					      			<merchant-calculations-url>'.$shipping_url.'</merchant-calculations-url>
					      		</merchant-calculations>
					    	</merchant-checkout-flow-support>';

			$xml = '<checkout-shopping-cart xmlns="http://checkout.google.com/schema/2">
					  <shopping-cart>'.$cart_xml.'</shopping-cart>
					  <checkout-flow-support>'.$shipping_xml.'</checkout-flow-support>
					</checkout-shopping-cart>';

			return $xml;
		}

		/**
		 * Returns price formatted as xml tag
		 *
		 * @param string $tag_name
		 * @param float $price
		 * @return string
		 */
		function getPriceXML($tag_name, $price)
		{
			$currency = $this->Application->RecallVar('curr_iso');
			return '<'.$tag_name.' currency="'.$currency.'">'.sprintf('%.2f', $price).'</'.$tag_name.'>';
		}

	    /**
	     * Calculates the cart's hmac-sha1 signature, this allows google to verify
	     * that the cart hasn't been tampered by a third-party.
	     *
	     * {@link http://code.google.com/apis/checkout/developer/index.html#create_signature}
	     *
	     * @param string $data the cart's xml
	     * @return string the cart's signature (in binary format)
	     */
	    function CalcHmacSha1($data, $gw_params) {
	      $key = $gw_params['merchant_key'];
	      $blocksize = 64;
	      $hashfunc = 'sha1';
	      if (mb_strlen($key) > $blocksize) {
	        $key = pack('H*', $hashfunc($key));
	      }
	      $key = str_pad($key, $blocksize, chr(0x00));
	      $ipad = str_repeat(chr(0x36), $blocksize);
	      $opad = str_repeat(chr(0x5c), $blocksize);
	      $hmac = pack(
	                    'H*', $hashfunc(
	                            ($key^$opad).pack(
	                                    'H*', $hashfunc(
	                                            ($key^$ipad).$data
	                                    )
	                            )
	                    )
	                );
	      return $hmac;
	    }

	    /**
	     * Returns XML request, that GoogleCheckout posts to notification / shipping calculation scripts
	     *
	     * @return string
	     */
	    function getRequestXML()
	    {
	    	$xml_data = $GLOBALS['HTTP_RAW_POST_DATA'];

	    	if ($this->Application->isDebugMode()) {
		    	$fp = fopen(FULL_PATH.'/xml_request.html', 'a');
				fwrite($fp, '--- '.adodb_date('Y-m-d H:i:s').' ---'."\n".$xml_data);
				fclose($fp);
	    	}

	    	return $xml_data;

	    	// for debugging
	    	/*return '<order-state-change-notification xmlns="http://checkout.google.com/schema/2"
					    serial-number="c821426e-7caa-4d51-9b2e-48ef7ecd6423">
					    <google-order-number>434532759516557</google-order-number>
					    <new-financial-order-state>CHARGEABLE</new-financial-order-state>
					    <new-fulfillment-order-state>NEW</new-fulfillment-order-state>
					    <previous-financial-order-state>REVIEWING</previous-financial-order-state>
					    <previous-fulfillment-order-state>NEW</previous-fulfillment-order-state>
					    <timestamp>2007-03-19T15:06:29.051Z</timestamp>
					</order-state-change-notification>';*/
	    }

	    /**
	     * Processes notifications from google checkout
	     *
	     * @param Array $gw_params
	     * @return int
	     */
		function processNotification($gw_params)
		{
    		// parse xml & get order_id from there, like sella pay
    		$this->gwParams = $gw_params;

    		$xml_helper =& $this->Application->recallObject('kXMLHelper');
    		/* @var $xml_helper kXMLHelper */

			$root_node =& $xml_helper->Parse( $this->getRequestXML() );
    		/* @var $root_node kXMLNode */

    		$this->Application->XMLHeader();
			define('DBG_SKIP_REPORTING', 1);

			$order_approvable = false;

			switch ($root_node->Name) {
				case 'MERCHANT-CALCULATION-CALLBACK':
					$xml_responce = $this->getShippingXML($root_node);
					break;

				case 'NEW-ORDER-NOTIFICATION':
				case 'RISK-INFORMATION-NOTIFICATION':
				case 'ORDER-STATE-CHANGE-NOTIFICATION':
					// http://code.google.com/apis/checkout/developer/Google_Checkout_XML_API_Notification_API.html#new_order_notifications
					list ($order_approvable, $xml_responce) = $this->getNotificationResponceXML($root_node);
					break;
			}

			echo $xml_responce;

			if ($this->Application->isDebugMode()) {
	    		$fp = fopen(FULL_PATH.'/xml_responce.html', 'a');
				fwrite($fp, '--- '.adodb_date('Y-m-d H:i:s').' ---'."\n".$xml_responce."\n");
				fclose($fp);
			}

    		return $order_approvable ? 1 : 0;
		}

		/**
		 * Processes notification
		 *
		 * @param kXMLNode $root_node
		 */
		function getNotificationResponceXML(&$root_node)
		{
			// we can get notification type by "$root_node->Name"

			$order_approvable = false;
			switch ($root_node->Name) {
				case 'NEW-ORDER-NOTIFICATION':
					$order_approvable = $this->processNewOrderNotification($root_node);
					break;

				case 'RISK-INFORMATION-NOTIFICATION':
					$order_approvable = $this->processRiskInformationNotification($root_node);
					break;

				case 'ORDER-STATE-CHANGE-NOTIFICATION':
					$order_approvable = $this->processOrderStateChangeNotification($root_node);
					break;
			}



			// !!! globally set order id, so gw_responce.php will not fail in setting TransactionStatus

			// 1. receive new order notification
			// put address & payment type in our order using id found in merchant-private-data (Make order status: Incomplete)

			// 2. receive risk information
			// don't know what to do, just mark order some how (Make order status: Incomplete)

			// 3. receive status change notification to CHARGEABLE (Make order status: Pending)
			// only mark order status

			// 4. admin approves order
			// make api call, that changes order state (fulfillment-order-state) to PROCESSING or DELIVERED (see manual)

			// 5. admin declines order
			// make api call, that changes order state (fulfillment-order-state) to WILL_NOT_DELIVER

			// Before you ship the items in an order, you should ensure that you have already received the new order notification for the order,
			// the risk information notification for the order and an order state change notification informing you that the order's financial
			// state has been updated to CHARGEABLE

			return Array ($order_approvable, '<notification-acknowledgment xmlns="http://checkout.google.com/schema/2" serial-number="'.$root_node->Attributes['SERIAL-NUMBER'].'" />');
		}

		/**
		 * Returns shipping calculations and places part of shipping address into order (1st step)
		 *
		 * http://code.google.com/apis/checkout/developer/Google_Checkout_XML_API_Merchant_Calculations_API.html#Returning_Merchant_Calculation_Results
		 *
		 * @param kXMLNode $node
		 * @return string
		 */
		function getShippingXML(&$root_node)
		{
			// 1. extract data from xml
			$search_nodes = Array (
				'SHOPPING-CART:MERCHANT-PRIVATE-DATA',
				'CALCULATE:ADDRESSES:ANONYMOUS-ADDRESS',
				'CALCULATE:SHIPPING',
			);

			foreach ($search_nodes as $search_string) {
				$found_node =& $root_node;
				/* @var $found_node kXMLNode */

				$search_string = explode(':', $search_string);
				foreach ($search_string as $search_node) {
					$found_node =& $found_node->FindChild($search_node);
				}

				$node_data = Array ();
				$sub_node =& $found_node->firstChild;
				/* @var $sub_node kXMLNode */

				do {
					if ($found_node->Name == 'SHIPPING') {
						$node_data[] = $sub_node->Attributes['NAME'];
					}
					else {
						$node_data[$sub_node->Name] = $sub_node->Data;
					}
				} while ( ($sub_node =& $sub_node->NextSibling()) );


				switch ($found_node->Name) {
					case 'MERCHANT-PRIVATE-DATA':
						$order_id = $node_data['ORDER_ID'];
						$session_id = $node_data['SESSION_ID'];
						break;

					case 'ANONYMOUS-ADDRESS':
						$address_info = $node_data;
						$address_id = $found_node->Attributes['ID'];
						break;

					case 'SHIPPING':
						$process_shippings = $node_data;
						break;
				}
			}

			// 2. update shipping address in order
			$order =& $this->Application->recallObject('ord', null, Array ('skip_autoload' => true));
			/* @var $order OrdersItem */

			$order->Load($order_id);

			$shipping_address = Array (
				'ShippingCity' => $address_info['CITY'],
				'ShippingState' => $address_info['REGION'],
				'ShippingZip' => $address_info['POSTAL-CODE'],
			);

			$cs_helper =& $this->Application->recallObject('CountryStatesHelper');
			/* @var $cs_helper kCountryStatesHelper */

			$shipping_address['ShippingCountry'] = $cs_helper->getCountryIso($address_info['COUNTRY-CODE'], true);

			$order->SetDBFieldsFromHash($shipping_address);
			$order->Update();

			// 3. get shipping rates based on given address

			$shipping_types_xml = '';
			$shipping_types = $this->getOrderShippings($order);

			// add available shipping types
			foreach ($shipping_types as $shipping_type) {
				$shipping_name = $shipping_type['ShippingName'];
				$processable_shipping_index = array_search($shipping_name, $process_shippings);
				if ($processable_shipping_index !== false) {
					$shipping_types_xml .= '<result shipping-name="'.htmlspecialchars($shipping_name).'" address-id="'.$address_id.'">
				    	        				<shipping-rate currency="USD">'.sprintf('%01.2f', $shipping_type['TotalCost']).'</shipping-rate>
				        	    				<shippable>true</shippable>
				        					</result>';

					// remove available shipping type from processable list
					unset($process_shippings[$processable_shipping_index]);
				}
			}

			// add unavailable shipping types
			foreach ($process_shippings as $shipping_name) {
				$shipping_types_xml .= '<result shipping-name="'.htmlspecialchars($shipping_name).'" address-id="'.$address_id.'">
											<shipping-rate currency="USD">0.00</shipping-rate>
				            				<shippable>false</shippable>
				        				</result>';
			}

			$shipping_types_xml = '<?xml version="1.0" encoding="UTF-8"?>
									<merchant-calculation-results xmlns="http://checkout.google.com/schema/2">
				  						<results>'.$shipping_types_xml.'</results>
									</merchant-calculation-results>';
			return $shipping_types_xml;
		}

		/**
		 * Places all information from google checkout into order (2nd step)
		 *
		 * @param kXMLNode $root_node
		 */
		function processNewOrderNotification(&$root_node)
		{
			// 1. extract data from xml
			$search_nodes = Array (
				'SHOPPING-CART:MERCHANT-PRIVATE-DATA',
				'ORDER-ADJUSTMENT:SHIPPING:MERCHANT-CALCULATED-SHIPPING-ADJUSTMENT',
				'BUYER-ID',
				'GOOGLE-ORDER-NUMBER',
				'BUYER-SHIPPING-ADDRESS',
				'BUYER-BILLING-ADDRESS',
			);

			$user_address = Array ();
			foreach ($search_nodes as $search_string) {
				$found_node =& $root_node;
				/* @var $found_node kXMLNode */

				$search_string = explode(':', $search_string);
				foreach ($search_string as $search_node) {
					$found_node =& $found_node->FindChild($search_node);
				}

				$node_data = Array ();
				if ($found_node->Children) {
					$sub_node =& $found_node->firstChild;
					/* @var $sub_node kXMLNode */

					do {
						$node_data[$sub_node->Name] = $sub_node->Data;
					} while ( ($sub_node =& $sub_node->NextSibling()) );
				}

				switch ($found_node->Name) {
					case 'MERCHANT-PRIVATE-DATA':
						$order_id = $node_data['ORDER_ID'];
						$session_id = $node_data['SESSION_ID'];
						break;

					case 'MERCHANT-CALCULATED-SHIPPING-ADJUSTMENT':
						$shpipping_info = $node_data;
						break;

					case 'BUYER-ID':
						$buyer_id = $found_node->Data;
						break;

					case 'GOOGLE-ORDER-NUMBER':
						$google_order_number = $found_node->Data;
						break;

					case 'BUYER-SHIPPING-ADDRESS':
						$user_address['Shipping'] = $node_data;
						break;

					case 'BUYER-BILLING-ADDRESS':
						$user_address['Billing'] = $node_data;
						break;
				}
			}

			// 2. update shipping address in order
			$order =& $this->Application->recallObject('ord', null, Array ('skip_autoload' => true));
			/* @var $order OrdersItem */

			$order->Load($order_id);

			if (!$order->isLoaded()) {
				return false;
			}

			// 2.1. this is 100% notification from google -> mark order with such payment type
			$order->SetDBField('PaymentType', $this->Application->GetVar('payment_type_id'));

			$this->parsed_responce = Array (
				'GOOGLE-ORDER-NUMBER' => $google_order_number,
				'BUYER-ID' => $buyer_id
			);

			// 2.2. save google checkout order information (maybe needed for future notification processing)
			$order->SetDBField('GWResult1', serialize($this->parsed_responce));
			$order->SetDBField('GoogleOrderNumber', $google_order_number);

			// 2.3. set user-selected shipping type
			$shipping_types = $this->getOrderShippings($order);

			foreach ($shipping_types as $shipping_type) {
				if ($shipping_type['ShippingName'] == $shpipping_info['SHIPPING-NAME']) {
					$order->SetDBField('ShippingInfo', serialize(Array (1 => $shipping_type))); // minimal package number is 1
					$order->SetDBField('ShippingCost', $shipping_type['TotalCost']); // set total shipping cost
					break;
				}
			}

			// 2.4. set full shipping & billing address
			$address_mapping = Array (
				'CONTACT-NAME' => 'To',
				'COMPANY-NAME' => 'Company',
				'EMAIL' => 'Email',
				'PHONE' => 'Phone',
				'FAX' => 'Fax',
				'ADDRESS1' => 'Address1',
				'ADDRESS2' => 'Address2',
				'CITY' => 'City',
				'REGION' => 'State',
				'POSTAL-CODE' => 'Zip',
			);

			$cs_helper =& $this->Application->recallObject('CountryStatesHelper');
			/* @var $cs_helper kCountryStatesHelper */

			foreach ($user_address as $field_prefix => $address_details) {
				foreach ($address_mapping as $src_field => $dst_field) {
					$order->SetDBField($field_prefix.$dst_field, $address_details[$src_field]);
				}

				if (!$order->GetDBField($field_prefix.'Phone')) {
					$order->SetDBField($field_prefix.'Phone', '-'); // required field
				}

				$order->SetDBField( $field_prefix.'Country', $cs_helper->getCountryIso($address_details['COUNTRY-CODE'], true) );
			}

			$order->SetDBField('OnHold', 1);
			$order->SetDBField('Status', ORDER_STATUS_PENDING);

			$order->Update();

			// unlink order, that GoogleCheckout used from shopping cart on site
			$sql = 'DELETE
					FROM '.TABLE_PREFIX.'SessionData
					WHERE VariableName = "ord_id" AND VariableValue = '.$order->GetID();
			$this->Conn->Query($sql);

			// simulate visiting shipping screen
			$sql = 'UPDATE '.TABLE_PREFIX.'OrderItems
					SET PackageNum = 1
					WHERE OrderId = '.$order->GetID();
			$this->Conn->Query($sql);

			return false;
		}

		/**
		 * Saves risk information in order record (3rd step)
		 *
		 * @param kXMLNode $root_node
		 */
		function processRiskInformationNotification(&$root_node)
		{
			// 1. extract data from xml
			$search_nodes = Array (
				'GOOGLE-ORDER-NUMBER',
				'RISK-INFORMATION',
			);

			foreach ($search_nodes as $search_string) {
				$found_node =& $root_node;
				/* @var $found_node kXMLNode */

				$search_string = explode(':', $search_string);
				foreach ($search_string as $search_node) {
					$found_node =& $found_node->FindChild($search_node);
				}

				$node_data = Array ();
				if ($found_node->Children) {
					$sub_node =& $found_node->firstChild;
					/* @var $sub_node kXMLNode */

					do {
						$node_data[$sub_node->Name] = $sub_node->Data;
					} while ( ($sub_node =& $sub_node->NextSibling()) );
				}

				switch ($found_node->Name) {
					case 'GOOGLE-ORDER-NUMBER':
						$google_order_number = $found_node->Data;
						break;

					case 'RISK-INFORMATION':
						$risk_information = $node_data;
						unset( $risk_information['BILLING-ADDRESS'] );
						break;
				}
			}

			// 2. update shipping address in order
			$order =& $this->Application->recallObject('ord', null, Array ('skip_autoload' => true));
			/* @var $order OrdersItem */

			$order->Load($google_order_number, 'GoogleOrderNumber');

			if (!$order->isLoaded()) {
				return false;
			}

			// 2.1. save risk information in order
			$this->parsed_responce = unserialize($order->GetDBField('GWResult1'));
			$this->parsed_responce = array_merge_recursive($this->parsed_responce, $risk_information);
			$order->SetDBField('GWResult1', serialize($this->parsed_responce));

			$order->Update();

			return false;
		}

		/**
		 * Perform PREAUTH/SALE type transaction direct from php script wihtout redirecting to 3rd-party website
		 *
		 * @param Array $item_data
		 * @param Array $gw_params
		 * @return bool
		 */
		function DirectPayment($item_data, $gw_params)
		{
			$this->gwParams = $gw_params;

			if ($gw_params['shipping_control'] == SHIPPING_CONTROL_PREAUTH) {
				// when shipping control is Pre-Authorize -> do nothing and charge when admin approves order
				return true;
			}

			$this->_chargeOrder($item_data);

			return false;
		}

		/**
		 * Issue charge-order api call
		 *
		 * @param Array $item_data
		 * @return bool
		 */
		function _chargeOrder($item_data)
		{
			$charge_xml = '	<charge-order xmlns="http://checkout.google.com/schema/2" google-order-number="'.$item_data['GoogleOrderNumber'].'">
			    				<amount currency="USD">'.sprintf('%.2f', $item_data['TotalAmount']).'</amount>
							</charge-order>';

			$root_node =& $this->executeAPICommand($charge_xml);

    		$this->parsed_responce = unserialize($item_data['GWResult1']);

    		if ($root_node->Name == 'REQUEST-RECEIVED') {
				$this->parsed_responce['FINANCIAL-ORDER-STATE'] = 'CHARGING';
				return true;
    		}

    		return false;
		}

		/**
		 * Perform SALE type transaction direct from php script wihtout redirecting to 3rd-party website
		 *
		 * @param Array $item_data
		 * @param Array $gw_params
		 * @return bool
		 */
		function Charge($item_data, $gw_params)
		{
			$this->gwParams = $gw_params;

			if ($gw_params['shipping_control'] == SHIPPING_CONTROL_DIRECT) {
				// when shipping control is Direct Payment -> do nothing and auto-charge on notification received
				return true;
			}

			$this->_chargeOrder($item_data);

			$order =& $this->Application->recallObject('ord.-item', null, Array ('skip_autoload' => true));
			/* @var $order OrdersItem */

			$order->Load($item_data['OrderId']);
			if (!$order->isLoaded()) {
				return false;
			}

			$order->SetDBField('OnHold', 1);
			$order->Update();

			return false;
		}

		/**
		 * Executes API command for order and returns result
		 *
		 * @param string $command_xml
		 * @return kXMLNode
		 */
		function &executeAPICommand($command_xml)
		{
			$submit_url = $this->gwParams['submit_url'].'/request/Merchant/'.$this->gwParams['merchant_id'];

			$curl_helper =& $this->Application->recallObject('CurlHelper');
			/* @var $curl_helper kCurlHelper */

			$xml_helper =& $this->Application->recallObject('kXMLHelper');
    		/* @var $xml_helper kXMLHelper */

			$curl_helper->SetPostData($command_xml);
			$auth_options = Array (
				CURLOPT_USERPWD => $this->gwParams['merchant_id'].':'.$this->gwParams['merchant_key'],
			);
			$curl_helper->setOptions($auth_options);

			$xml_responce = $curl_helper->Send($submit_url);

			$root_node =& $xml_helper->Parse($xml_responce);
    		/* @var $root_node kXMLNode */

    		return $root_node;
		}

		/**
		 * Marks order as pending, when it's google status becomes CHARGEABLE (4th step)
		 *
		 * @param kXMLNode $root_node
		 */
		function processOrderStateChangeNotification(&$root_node)
		{
			// 1. extract data from xml
			$search_nodes = Array (
				'GOOGLE-ORDER-NUMBER',
				'NEW-FINANCIAL-ORDER-STATE',
				'PREVIOUS-FINANCIAL-ORDER-STATE',
			);

			$order_state = Array ();
			foreach ($search_nodes as $search_string) {
				$found_node =& $root_node;
				/* @var $found_node kXMLNode */

				$search_string = explode(':', $search_string);
				foreach ($search_string as $search_node) {
					$found_node =& $found_node->FindChild($search_node);
				}

				switch ($found_node->Name) {
					case 'GOOGLE-ORDER-NUMBER':
						$google_order_number = $found_node->Data;
						break;

					case 'NEW-FINANCIAL-ORDER-STATE':
						$order_state['new'] = $found_node->Data;
						break;

					case 'PREVIOUS-FINANCIAL-ORDER-STATE':
						$order_state['old'] = $found_node->Data;
						break;
				}
			}

			// 2. update shipping address in order
			$order =& $this->Application->recallObject('ord', null, Array ('skip_autoload' => true));
			/* @var $order OrdersItem */

			$order->Load($google_order_number, 'GoogleOrderNumber');

			if (!$order->isLoaded()) {
				return false;
			}

			$state_changed = ($order_state['old'] != $order_state['new']);

			if ($state_changed) {
				$order_charged = ($order_state['new'] == 'CHARGED') && ($order->GetDBField('Status') == ORDER_STATUS_PENDING);

				$this->parsed_responce = unserialize($order->GetDBField('GWResult1'));
				$this->parsed_responce['FINANCIAL-ORDER-STATE'] = $order_state['new'];
				$order->SetDBField('GWResult1', serialize($this->parsed_responce));

				if ($order_charged) {
					// when using Pre-Authorize
					$order->SetDBField('OnHold', 0);
				}

				$order->Update();

				if ($order_charged) {
					// when using Pre-Authorize
					$order_eh =& $this->Application->recallObject('ord_EventHandler');
					/* @var $order_eh OrdersEventHandler */

					$order_eh->SplitOrder( new kEvent('ord:OnMassOrderApprove'), $order);
				}
			}

			// update order record in "google_checkout_notify.php" only when such state change happens
			$order_chargeable = ($order_state['new'] == 'CHARGEABLE') && $state_changed;

			if ($order_chargeable) {
				if ($this->gwParams['shipping_control'] == SHIPPING_CONTROL_PREAUTH) {
					$order->SetDBField('OnHold', 0);
					$order->Update();
				}

				$process_xml = '<process-order xmlns="http://checkout.google.com/schema/2" google-order-number="'.$order->GetDBField('GoogleOrderNumber').'"/>';
				$root_node =& $this->executeAPICommand($process_xml);
			}

			return $order_chargeable;
		}

		/**
		 * Retrieves shipping types available for given order
		 *
		 * @param OrdersItem $order
		 * @return Array
		 */
		function getOrderShippings(&$order)
		{
			$weight_sql = 'IF(oi.Weight IS NULL, 0, oi.Weight * oi.Quantity)';

			$query = '	SELECT
							SUM(oi.Quantity) AS TotalItems,
							SUM('.$weight_sql.') AS TotalWeight,
							SUM(oi.Price * oi.Quantity) AS TotalAmount,
							SUM(oi.Quantity) - SUM(IF(p.MinQtyFreePromoShipping > 0 AND p.MinQtyFreePromoShipping <= oi.Quantity, oi.Quantity, 0)) AS TotalItemsPromo,
							SUM('.$weight_sql.') - SUM(IF(p.MinQtyFreePromoShipping > 0 AND p.MinQtyFreePromoShipping <= oi.Quantity, '.$weight_sql.', 0)) AS TotalWeightPromo,
							SUM(oi.Price * oi.Quantity) - SUM(IF(p.MinQtyFreePromoShipping > 0 AND p.MinQtyFreePromoShipping <= oi.Quantity, oi.Price * oi.Quantity, 0)) AS TotalAmountPromo
						FROM '.TABLE_PREFIX.'OrderItems oi
						LEFT JOIN '.TABLE_PREFIX.'Products p ON oi.ProductId = p.ProductId
						WHERE oi.OrderId = '.$order->GetID().' AND p.Type = 1';
			$shipping_totals = $this->Conn->GetRow($query);

			$this->Application->recallObject('ShippingQuoteEngine');
			$quote_engine_collector =& $this->Application->recallObject('ShippingQuoteCollector');
			/* @var $quote_engine_collector ShippingQuoteCollector */

			$shipping_quote_params = Array(
				'dest_country'	=>	$order->GetDBField('ShippingCountry'),
				'dest_state'	=>	$order->GetDBField('ShippingState'),
				'dest_postal'	=>	$order->GetDBField('ShippingZip'),
				'dest_city'		=>	$order->GetDBField('ShippingCity'),
				'dest_addr1'	=>	'',
				'dest_addr2'	=>	'',
				'dest_name'		=>	'user-' . $order->GetDBField('PortalUserId'),
				'packages' 		=>	Array(
										Array(
											'package_key'	=>	'package1',
											'weight'		=>	$shipping_totals['TotalWeight'],
											'weight_unit'	=>	'KG',
											'length'		=>	'',
											'width'			=>	'',
											'height'		=>	'',
											'dim_unit'		=>	'IN',
											'packaging'		=>	'BOX',
											'contents'		=>	'OTR',
											'insurance'		=>	'0'
										),
									),
				'amount'		=>	$shipping_totals['TotalAmount'],
				'items'			=>	$shipping_totals['TotalItems'],
				'limit_types' 	=> 	serialize(Array ('ANY')),

				'promo_params'	=>	Array (
					'items'		=>	$shipping_totals['TotalItemsPromo'],
					'amount'	=>	$shipping_totals['TotalAmountPromo'],
					'weight'	=>	$shipping_totals['TotalWeightPromo'],
				),
			);

			return $quote_engine_collector->GetShippingQuotes($shipping_quote_params);
		}

		/**
		 * Returns gateway responce from last operation
		 *
		 * @return string
		 */
		function getGWResponce()
		{
			return serialize($this->parsed_responce);
		}

		/**
		 * Informs payment gateway, that order has been shipped
		 *
		 * http://code.google.com/apis/checkout/developer/Google_Checkout_XML_API_Order_Level_Shipping.html#Deliver_Order
		 *
		 * @param Array $item_data
		 * @param Array $gw_params
		 * @return bool
		 */
		function OrderShipped($item_data, $gw_params)
		{
			$this->gwParams = $gw_params;

			$shipping_info = unserialize($item_data['ShippingInfo']);
			if (getArrayValue($shipping_info, 'Code')) {
				$traking_carrier = '<carrier>'.$item_data['Code'].'</carrier>';
			}

			if ($item_data['ShippingTracking']) {
				$tracking_data = '<tracking-data>'.$traking_carrier.'
        							 <tracking-number>'.$item_data['ShippingTracking'].'</tracking-number>
        						  </tracking-data>';
			}

			$ship_xml = '	<deliver-order xmlns="http://checkout.google.com/schema/2" google-order-number="'.$item_data['GoogleOrderNumber'].'">
								'.$traking_data.'
    							<send-email>true</send-email>
							</deliver-order>';
			$root_node =& $this->executeAPICommand($ship_xml);
		}

		/**
		 * Informs payment gateway, that order has been declined
		 *
		 * @param Array $item_data
		 * @param Array $gw_params
		 * @return bool
		 */
		function OrderDeclined($item_data, $gw_params)
		{

		}
	}