/**
	 * Internal method to handles a gateway notification:
	 *
	 * @param  array                $postdata         'x_response_code', 'x_response_subcode', 'x_response_reason_code', 'x_response_reason_text', 'x_auth_code', 'x_avs_code', 'x_trans_id', 'x_invoice_num', 'x_description', 'x_amount', 'x_method', 'x_type', 'x_cust_id', 'x_first_name', 'x_last_name', 'x_company', 'x_address', 'x_city', 'x_state', 'x_zip', 'x_country', 'x_phone', 'x_fax', 'x_email', 'x_ship_to_first_name', 'x_ship_to_last_name', 'x_ship_to_company', 'x_ship_to_address', 'x_ship_to_city', 'x_ship_to_state', 'x_ship_to_zip', 'x_ship_to_country', 'x_tax', 'x_duty', 'x_freight', 'x_tax_exempt', 'x_po_num', 'x_MD5_Hash', 'x_cavv_response', 'x_test_request', 'x_subscription_id', 'x_subscription_paynum'
	 * @return boolean
	 */
	private function _handleNotifyInternal( $postdata )
	{
		global $_CB_framework, $_CB_database;

		// Only ARB notifications have: x_subscription_id (Subscription ID) and the x_subscription_paynum (Subscription Payment Number)

		// check that required params are here:
		static $mandatory	=	array( 'x_cust_id', 'x_response_code', 'x_trans_id', 'x_amount', 'x_po_num', 'x_MD5_Hash' );
		foreach ( $mandatory as $v ) {
			if ( ! isset( $postdata[$v] ) ) {
				$ipn						=&	$this->_prepareIpn( 'D', '', '', 'ARB-Silent: mandatory param missing', null, 'utf-8' );
				$ipn->raw_data				=	'$message_type="' . addslashes( cbGetParam( $postdata, 'x_type' ) ) . "\";\n"
											.	/* cbGetParam() not needed: we want raw info */ '$_GET=' . var_export( $_GET, true ) . ";\n"
											.	/* cbGetParam() not needed: we want raw info */ '$_POST=' . var_export( $_POST, true ) . ";\n"
											;
				$ipn->store();

				return false;
			}
		}

		// check MD5 hash:
		if ( ! $this->_checkHashARBsilent( $postdata ) ) {
			$ipn							=&	$this->_prepareIpn( 'O', '', '', 'ARB-Silent: ARB silent post Hash check failed', null, 'utf-8' );
			$ipn->raw_data					=	'$message_type="' . addslashes( cbGetParam( $postdata, 'x_type' ) ) . "\";\n"
											.	/* cbGetParam() not needed: we want raw info */ '$_GET=' . var_export( $_GET, true ) . ";\n"
											.	/* cbGetParam() not needed: we want raw info */ '$_POST=' . var_export( $_POST, true ) . ";\n"
											;
			$ipn->store();

			return false;
		}

/*
x_response_code			=	1
x_response_subcode		=	1
x_response_reason_code	=	1
x_response_reason_text	=	This+transaction+has+been+approved%2E
x_auth_code				=	QbJHm4
x_avs_code				=	Y
x_trans_id				=	2147490176
x_invoice_num			=	INV12345					<--------
x_description			=	My+test+description
x_amount				=	0%2E44
x_method				=	CC
x_type					=	auth%5Fcapture
x_cust_id				=	CustId                      <--------
x_first_name			=	Firstname
x_last_name				=	LastNamenardkkwhczdp
x_company				=	
x_address				=	
x_city					=	
x_state					=	
x_zip					=	
x_country				=	
x_phone					=	
x_fax					=	
x_email					=	
x_ship_to_first_name	=	
x_ship_to_last_name		=	
x_ship_to_company		=	
x_ship_to_address		=	
x_ship_to_city			=	
x_ship_to_state			=	
x_ship_to_zip			=	
x_ship_to_country		=	
x_tax					=	0%2E0000
x_duty					=	0%2E0000
x_freight				=	0%2E0000
x_tax_exempt			=	FALSE
x_po_num				=	                           <-------- not in ARB
x_MD5_Hash				=	B9B3D19AEFD7BECC86C5FB3DB717D565
x_cavv_response			=	2
x_test_request			=	false
x_subscription_id		=	101635
x_subscription_paynum	=	1
*/
		// For ARB we have:
		$mandatory							=	array( 'x_test_request', 'x_subscription_id', 'x_subscription_paynum' );
		// For AIM we have: 'cb_custom' (paymentbasket id)

		// For now ignore non-ARB silent posts:
		foreach ( $mandatory as $v ) {
			if ( ! isset( $postdata[$v] ) ) {
				// This is not an ARB silent post: silently ignore it:
				return true;
			}
		}

		// get the mandatory params, such as x_cust_id:
		$x_cust_id							=	cbGetParam( $postdata, 'x_cust_id' );
		$x_subscription_id					=	cbGetParam( $postdata, 'x_subscription_id', 0 );
		$x_type								=	strtoupper( cbGetParam( $postdata, 'x_type' ) );
		$x_response_code					=	cbGetParam( $postdata, 'x_response_code' );
		// $x_response_subcode					=	cbGetParam( $postdata, 'x_response_subcode' );
		$x_response_reason_code				=	cbGetParam( $postdata, 'x_response_reason_code' );
		$x_response_reason_text				=	cbGetParam( $postdata, 'x_response_reason_text' );
		$x_invoice_num						=	cbGetParam( $postdata, 'x_invoice_num' );
		$x_trans_id							=	cbGetParam( $postdata, 'x_trans_id' );
		$x_amount							=	cbGetParam( $postdata, 'x_amount' );
		$x_method							=	cbGetParam( $postdata, 'x_method' );
		if ( ( ! $x_trans_id ) || ( ! $x_invoice_num ) || ( ! $x_cust_id ) || ( ! $x_subscription_id ) || ( ! $x_type ) || ( ! $x_response_code ) || ( ! $x_response_reason_code ) ) {
			return false;
		}

		// Log the INS:
		$log_type							=	'I';
		$paymentStatus						=	$this->_paymentStatus( $x_type, $x_response_code );
		$reasonCode							=	null;
		$paymentType						=	( $x_method == 'CC' ? 'Credit Card' : ( $x_method == 'ECHECK' ? 'E-Check' : $x_method ) );
		$reasonText							=	( $x_response_code != 1 ? $x_response_code . ': ' . $x_response_reason_text : null );		//TODO do it in normal AIM and ARB too
		$ipn								=&	$this->_prepareIpn( $log_type, $paymentStatus, $paymentType, $reasonText, $_CB_framework->now(), 'utf-8' );
		$ipn->raw_data						=	'$message_type="' . $x_type . "\";\n"
											.	/* cbGetParam() not needed: we want raw info */ '$_GET=' . var_export( $_GET, true ) . ";\n"
											.	/* cbGetParam() not needed: we want raw info */ '$_POST=' . var_export( $_POST, true ) . ";\n"
											;
		$ipn->user_id						=	(int) $x_cust_id;
		$ipn->payment_status				=	$paymentStatus;
		if ( ( $paymentStatus == 'Pending' ) && ( $x_response_code == 4 ) ) {
			$ipn->pending_reason			=	'paymentreview';		// held for payment review
		}
				
		// handle the message:
		switch ( $x_type ) {				// The type of credit card transaction: AUTH_CAPTURE, AUTH_ONLY, CAPTURE_ONLY, CREDIT, PRIOR_AUTH_CAPTURE, VOID
			case 'AUTH_CAPTURE':			// The amount is sent for authorization, and if approved, is automatically submitted for settlement.
			case 'CAPTURE_ONLY':			// This transaction type is used to complete a previously authorized transaction that was not originally submitted through the payment gateway or that requires voice authorization.
			case 'PRIOR_AUTH_CAPTURE':		// An Authorization Only and a Prior Authorization and Capture together are considered one complete transaction.
			case 'VOID':					// This transaction type is used to cancel an original transaction that is not yet settled and prevents it from being sent for settlement.
			case 'CREDIT':					// This transaction type is used to refund a customer for a transaction that was originally processed and successfully settled through the payment gateway.
			case 'AUTH_ONLY':				// This transaction type is sent for authorization only. The transaction will not be sent for settlement until the credit card transaction type Prior Authorization and Capture (see definition below) is submitted, or the transaction is submitted for capture manually in the Merchant Interface.
				$paymentBasket				=	new cbpaidPaymentBasket( $_CB_database );
				$basketLoaded				=	$paymentBasket->loadThisMatching( array( 'user_id' => (int) $x_cust_id, 'subscr_id' => (string) $x_subscription_id ) );
				if ( $basketLoaded ) {
					$ipn->bindBasket( $paymentBasket );
					$ipn->test_ipn			=	$paymentBasket->test_ipn;
				} else {
					$ipn->mc_gross			=	$x_amount;
					$ipn->mc_currency		=	'USD';
				}
				if ( in_array( $x_type, array( 'CREDIT', 'VOID' ) ) ) {
					$ipn->mc_gross			=	- $ipn->mc_gross;
				}
				$ipn->txn_id				=	$x_trans_id;
				$ipn->payment_date			=	date( 'H:i:s M d, Y T', $_CB_framework->now() );		// Paypal-style
				if ( $x_subscription_id ) {
					$ipn->txn_type			=	'subscr_payment';
					$ipn->subscr_id			=	$x_subscription_id;
				} else {
					$ipn->txn_type			=	'web_accept';
				}

				$ipn->raw_result			=	'VERIFIED';
				if ( $basketLoaded ) {
					// Account for the payment: if it is a subscription, then it is of type "auto-renewing with processor notifications" anyway:
					$autorecurring_type		=	( $x_subscription_id ? 2 : 0 );		// 0: not auto-recurring, 1: auto-recurring without payment processor notifications, 2: auto-renewing with processor notifications updating $expiry_date
					$autorenew_type			=	( $x_subscription_id ? 2 : 0 );		// 0: not auto-renewing (manual renewals), 1: asked for by user, 2: mandatory by configuration
					$this->updatePaymentStatus( $paymentBasket, $ipn->txn_type, $paymentStatus, $ipn, 1, $autorecurring_type, $autorenew_type, false );
					$ret					=	true;
				} else {
					$ret					=	false;
				}
				break;
			default:
				$ipn->log_type				=	'T';
				$ipn->reason_code			=	'Unexpected x_type';
				$ret						=	false;
		}
		$ipn->store();

		return $ret;
	}
	/**
	 * Handles a gateway notification
	 *
	 * @param cbpaidPaymentBasket $paymentBasket
	 * @param array $postdata
	 * @return bool
	 */
	protected function handleNotify( $paymentBasket, $postdata )
	{
		global $_CB_framework;

		$transactionId												=	cbGetParam( $postdata, 'txn_id', null );
		$subscriptionId												=	cbGetParam( $postdata, 'recurring_payment_id', null );
		$return														=	false;

		if ( $transactionId || $subscriptionId ) {
			if ( $this->hasPaypalApi() ) {
				$paymentStatus										=	cbGetParam( $postdata, 'payment_status', null );
				$paymentType										=	cbGetParam( $postdata, 'payment_type', null );

				$ipn												=&	$this->_prepareIpn( 'I', $paymentStatus, $paymentType, null, $_CB_framework->now(), 'utf-8' );

				if ( $subscriptionId ) {
					$exists											=	$paymentBasket->loadThisMatching( array( 'subscr_id' => $subscriptionId ) );
				} else {
					$custom											=	(int) cbGetParam( $postdata, 'custom', null );

					if ( $custom ) {
						$exists										=	$paymentBasket->load( $custom );
					} else {
						$exists										=	$paymentBasket->loadThisMatching( array( 'txn_id' => $transactionId ) );
					}
				}

				if ( $exists ) {
					$ipn->bindBasket( $paymentBasket );

					$ipn->user_id									=	(int) $paymentBasket->user_id;
				}

				$ipn->bind( $postdata );

				if ( $exists ) {
					$ipn->item_number								=	$paymentBasket->item_number;
				}

				if ( $subscriptionId ) {
					if ( ! $ipn->payment_status ) {
						$profileStatus								=	cbGetParam( $postdata, 'profile_status', null );

						if ( $profileStatus == 'Cancelled' ) {
							$ipn->payment_status					=	'Unsubscribed';
							$ipn->payment_date						=	cbGetParam( $postdata, 'time_created', null );
						} elseif ( $profileStatus == 'Active' ) {
							$ipn->payment_status					=	'Completed';
							$ipn->payment_date						=	cbGetParam( $postdata, 'time_created', null );
						} elseif ( $profileStatus == 'Expired' ) {
							$ipn->payment_status					=	'Unsubscribed';
							$ipn->payment_date						=	cbGetParam( $postdata, 'time_created', null );
						} elseif ( $profileStatus == 'Suspended' ) {
							$ipn->payment_status					=	'Denied';
							$ipn->payment_date						=	cbGetParam( $postdata, 'time_created', null );
						} elseif ( $profileStatus == 'Pending' ) {
							$ipn->payment_status					=	'Pending';
							$ipn->payment_date						=	cbGetParam( $postdata, 'time_created', null );
						}
					}

					$requestParams									=	array(	'METHOD' => 'GetRecurringPaymentsProfileDetails',
																				'PROFILEID' => $subscriptionId
																			);
				} else {
					$requestParams									=	array(	'METHOD' => 'GetTransactionDetails',
																				'TRANSACTIONID' => $transactionId
																			);
				}

				$this->_signRequestParams( $requestParams );

				$results											=	array();
				$response											=	null;
				$status												=	null;
				$error												=	$this->_httpsRequest( str_replace( 'www', 'api-3t', $this->gatewayUrl( 'psp' ) . '/nvp' ), $requestParams, 105, $response, $status, 'post', 'normal' );

				if ( $response ) {
					parse_str( $response, $results );
				}

				$rawData											=	'$response="' . preg_replace( '/([^\s]{100})/', '$1 ', $response ) . "\"\n"
																	.	'$results=' . var_export( $results, true ) . ";\n"
																	.	'$_GET=' . var_export( $_GET, true ) . ";\n"
																	.	'$_POST=' . var_export( $_POST, true ) . ";\n";

				$ipn->setRawData( $rawData );

				if ( $error || ( $status != 200 ) || ( ! $response ) ) {
					$ipn->log_type									=	'D';

					$ipn->setRawResult( 'COMMUNICATION ERROR' );

					$this->_setLogErrorMSG( 3, $ipn, $this->getPayName() . ' HTTPS POST request to payment gateway server failed.', CBPTXT::T( "Submitted transaction details request didn't return an error but didn't complete." ) . ' ' . CBPTXT::T( 'Please contact site administrator to check error log.' ) );
				} else {
					$insToIpn										=	array(	'address_street' => 'STREET',
																				'address_city' => 'CITY',
																				'address_state' => 'STATE',
																				'address_zip' => 'ZIP',
																				'address_country' => 'COUNTRY',
																				'address_country_code' => 'COUNTRYCODE',
																				'address_status' => 'ADDRESSSTATUS',
																				'first_name' => 'FIRSTNAME',
																				'last_name' => 'LASTNAME',
																				'payer_email' => 'EMAIL',
																				'payer_id' => 'PAYERID',
																				'payer_status' => 'PAYERSTATUS',
																				'auth_id' => 'CORRELATIONID',
																				'tax' => 'TAXAMT',
																				'mc_currency' => 'CURRENCYCODE',
																				'mc_fee' => 'FEEAMT',
																				'mc_gross' => 'AMT'
																			);

					if ( $ipn->payment_status == 'Refunded' ) {
						unset( $insToIpn['mc_fee'] );
						unset( $insToIpn['mc_gross'] );
					}

					foreach ( $insToIpn as $k => $v ) {
						$apiValue									=	cbGetParam( $results, $v );

						if ( $apiValue && ( ! in_array( $apiValue, array( '0.00', 'None' ) ) ) ) {
							$ipn->$k								=	$apiValue;
						}
					}

					switch ( $ipn->txn_type ) {
						case 'recurring_payment':
							$ipn->txn_type							=	'subscr_payment';
							break;
						case 'recurring_payment_profile_created':
							$ipn->txn_type							=	'subscr_signup';
							break;
						case 'recurring_payment_profile_cancel':
							$ipn->txn_type							=	'subscr_cancel';
							break;
						case 'recurring_payment_expired':
							$ipn->txn_type							=	'subscr_eot';
							break;
						case 'recurring_payment_skipped':
							$ipn->txn_type							=	'subscr_failed';
							break;
					}

					$valid											=	$this->_validateIPN( $ipn, $paymentBasket, cbGetParam( $_REQUEST, 'cbpid' ) );

					if ( $valid === true ) {
						if ( cbGetParam( $results, 'ACK' ) == 'Success' ) {
							if ( $exists ) {
								$ipn->setRawResult( 'SUCCESS' );

								if ( $ipn->txn_type != 'subscr_signup' ) {
									$autorecurring_type				=	( in_array( $ipn->txn_type, array( 'subscr_payment', 'subscr_signup', 'subscr_modify', 'subscr_eot', 'subscr_cancel', 'subscr_failed' ) ) ? 2 : 0 );
									$autorenew_type					=	( $autorecurring_type ? ( ( ( $this->getAccountParam( 'enabled', 0 ) == 3 ) && ( $paymentBasket->isAnyAutoRecurring() == 2 ) ) ? 1 : 2 ) : 0 );

									if ( $autorecurring_type && ( $ipn->txn_type == 'subscr_signup' ) && ( ( $paymentBasket->period1 ) && ( $paymentBasket->mc_amount1 == 0 ) ) && ( $ipn->payment_status == '' ) ) {
										$ipn->payment_status		=	'Completed';
									}

									if ( ( $ipn->payment_status == 'Refunded' ) && ( $paymentBasket->mc_gross != ( - $ipn->mc_gross ) ) ) {
										$ipn->payment_status		=	'Partially-Refunded';
									}

									if ( in_array( $ipn->txn_type, array( 'subscr_eot', 'subscr_cancel', 'subscr_failed' ) ) ) {
										$autorecurring_type			=	0;
									}

									$this->updatePaymentStatus( $paymentBasket, $ipn->txn_type, $ipn->payment_status, $ipn, 1, $autorecurring_type, $autorenew_type, false );

									$return							=	true;

									$ipn->store();
								} else {
									$return							=	null;
								}
							} else {
								$ipn->log_type						=	'J';

								$ipn->setRawResult( 'FAILED' );
							}
						} else {
							$ipn->log_type							=	'M';

							$ipn->setRawResult( 'FAILED' );

							$this->_setLogErrorMSG( 3, $ipn, $this->getPayName() . ' Paypal API error returned. ERROR: ' . cbGetParam( $results, 'L_LONGMESSAGE0' ) . ' CODE: ' . cbGetParam( $results, 'L_ERRORCODE0' ), cbGetParam( $results, 'L_SHORTMESSAGE0' ) . '. ' . CBPTXT::T( 'Please contact site administrator to check error log.' ) );
						}
					} else {
						$ipn->log_type								=	'O';

						$ipn->setRawResult( 'MISMATCH' );

						$this->_setLogErrorMSG( 3, $ipn, $this->getPayName() . ' Paypal IPN fraud attempt. ERROR: ' . $valid, CBPTXT::T( 'Invalid transaction.' ) . ' ' . CBPTXT::T( 'Please contact site administrator to check error log.' ) );
					}
				}
			} else {
				$this->_setLogErrorMSG( 3, null, $this->getPayName() . ' Needed Paypal API username, password and signature not set.' . "\n" . '$_GET=' . var_export( $_GET, true ) . "\n" . '$_POST=' . var_export( $_POST, true ) . "\n", CBPTXT::T( 'Needed Paypal API username, password and signature not set.' ) . ' ' . CBPTXT::T( 'Please contact site administrator to check error log.' ) );
			}
		} else {
			$this->_setLogErrorMSG( 3, null, $this->getPayName() . ' Needed Paypal Transaction ID (txn_id) missing.' . "\n" . '$_GET=' . var_export( $_GET, true ) . "\n" . '$_POST=' . var_export( $_POST, true ) . "\n", CBPTXT::T( 'Transaction not found.' ) . ' ' . CBPTXT::T( 'Please contact site administrator to check error log.' ) );
		}

		return $return;
	}