Index: branches/5.3.x/units/gateways/gw_classes/google_checkout.php
===================================================================
--- branches/5.3.x/units/gateways/gw_classes/google_checkout.php	(revision 16226)
+++ branches/5.3.x/units/gateways/gw_classes/google_checkout.php	(revision 16227)
@@ -1,940 +1,940 @@
 <?php
 /**
 * @version	$Id$
 * @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>'.kUtil::escape($order_item['ProductName'], kUtil::ESCAPE_HTML).'</item-name>
 					        		<item-description>'.kUtil::escape($order_item[$ml_formatter->LangFieldName('DescriptionExcerpt')], kUtil::ESCAPE_HTML).'</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="'.kUtil::escape($shipping_name, kUtil::ESCAPE_HTML).'">
 										<price currency="USD">0.00</price>
 									</merchant-calculated-shipping>';
 			}
 
 			$use_ssl = substr($this->gwParams['submit_url'], 0, 8) == 'https://' ? true : null;
 			$shipping_url = $this->getNotificationUrl('units/gateways/gw_classes/notify_scripts/google_checkout_shippings.php', $use_ssl);
 
 			$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'];
+	    	$xml_data = file_get_contents('php://input');
 
 	    	if ( $this->Application->isDebugMode() ) {
 	    		$this->toLog($xml_data, 'xml_request.html');
 	    	}
 
 	    	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() ) {
 				$this->toLog($xml_responce, 'xml_responce.html');
 			}
 
     		return $order_approvable ? 1 : 0;
 		}
 
 		/**
 	     * Writes XML requests and responces to a file
 	     *
 	     * @param string $xml_data
 	     * @param string $xml_file
 	     */
 	    function toLog($xml_data, $xml_file)
 	    {
 	    	$fp = fopen( (defined('RESTRICTED') ? RESTRICTED : FULL_PATH) . '/' . $xml_file, 'a' );
 			fwrite($fp, '--- ' . date('Y-m-d H:i:s') . ' ---' . "\n" . $xml_data);
 			fclose($fp);
 	    }
 
 		/**
 		 * 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="'.kUtil::escape($shipping_name, kUtil::ESCAPE_HTML).'" 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="'.kUtil::escape($shipping_name, kUtil::ESCAPE_HTML).'" 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.'UserSessionData
 					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)
 		{
 
 		}
-	}
\ No newline at end of file
+	}