/**
  * Fills object with all standard items of a Notification record
  *
  * @param  cbpaidPayHandler     $payHandler
  * @param  int                  $test_ipn
  * @param  string               $log_type
  * @param  string               $paymentStatus
  * @param  string               $paymentType
  * @param  string               $reasonCode
  * @param  int                  $paymentTime
  * @param  string               $charset
  */
 public function initNotification($payHandler, $test_ipn, $log_type, $paymentStatus, $paymentType, $reasonCode, $paymentTime, $charset = 'utf-8')
 {
     $this->payment_method = $payHandler->getPayName();
     $this->gateway_account = $payHandler->getAccountParam('id');
     $this->log_type = $log_type;
     $this->time_received = Application::Database()->getUtcDateTime();
     $this->ip_addresses = cbpaidRequest::getIPlist();
     $this->geo_ip_country_code = cbpaidRequest::getGeoIpCountryCode();
     $this->notify_version = '2.1';
     $this->user_id = (int) cbGetParam($_GET, 'user', 0);
     $this->charset = $charset;
     $this->test_ipn = $test_ipn;
     $this->payer_status = 'unverified';
     $this->payment_status = $paymentStatus;
     if (in_array($paymentStatus, array('Completed', 'Pending', 'Processed', 'Failed', 'Reversed', 'Refunded', 'Partially-Refunded', 'Canceled_Reversal'))) {
         if (in_array($paymentStatus, array('Completed', 'Reversed', 'Refunded', 'Partially-Refunded', 'Canceled_Reversal'))) {
             $this->payment_date = gmdate('H:i:s M d, Y T', $paymentTime);
             // paypal-style
         }
         $this->payment_type = $paymentType;
     }
     if ($reasonCode) {
         $this->reason_code = $reasonCode;
     }
 }
	/**
	 * create a new subscription object and corresponding object in database
	 *
	 * @param int|null      $user_id                 CB user id
	 * @param cbpaidProduct $plan                    plan object of this subscription
	 * @param string        $status                  like status class variable
	 * @param boolean       $store                   true (default) if should be stored into db
	 * @param int           $subscriptionTime        time of subscription
	 */
	protected function createMerchandiseRecord( $user_id, &$plan, $status = 'R', $store = true, $subscriptionTime = null ) {
		global $_CB_framework;

		if ( $subscriptionTime === null ) {
			$subscriptionTime			=	$_CB_framework->now();
		}

		$this->reset();

		$this->user_id					=	$user_id;
		$this->plan_id					=	$plan->get( 'id' );
		$this->payment_date				=	date( 'Y-m-d H:i:s', $subscriptionTime );
		$this->getCurrencyAmount( $plan );
		$this->status					=	$status;
		if ( is_object( $plan->_integrations ) ) {
			$this->integrations				=	$plan->_integrations->asJson();
		} else {
			$this->integrations				=	'';
		}
		if ( $store ) {
			$this->ip_addresses			=	cbpaidRequest::getIPlist();
			$this->historySetMessage( $this->recordName() . ' record created' );
			$this->store();
		}
		$this->_plan 					=	$plan;
	}
 /**
  * If table key (id) is NULL : inserts a new row
  * otherwise updates existing row in the database table
  *
  * Can be overridden or overloaded by the child class
  *
  * @param  boolean  $updateNulls  TRUE: null object variables are also updated, FALSE: not.
  * @return boolean                TRUE if successful otherwise FALSE
  */
 public function store($updateNulls = false)
 {
     $key = $this->_tbl_key;
     if (!$this->{$key}) {
         $this->event_time = $this->_db->getUtcDateTime();
         $this->user_id = Application::MyUser()->getUserId();
         $this->ip_addresses = cbpaidRequest::getIPlist();
         $this->log_version = 1;
     }
     return parent::store($updateNulls);
 }
	/**
	 * If table key (id) is NULL : inserts a new row
	 * otherwise updates existing row in the database table
	 *
	 * Can be overridden or overloaded by the child class
	 *
	 * @param  boolean  $updateNulls  TRUE: null object variables are also updated, FALSE: not.
	 * @return boolean                TRUE if successful otherwise FALSE
	 */
	public function store( $updateNulls = false ) {
		global $_CB_framework;

		$key						=	$this->_tbl_key;
		if ( ! $this->$key ) {
			$this->event_time		=	date( 'Y-m-d H:i:s', $_CB_framework->now() );
			$this->user_id			=	$_CB_framework->myId();
			$this->ip_addresses		=	cbpaidRequest::getIPlist();
			$this->log_version		=	1;
		}
		return parent::store( $updateNulls );
	}
 /**
  * create a new subscription object and corresponding object in database
  *
  * @param int|null      $user_id                 CB user id
  * @param cbpaidProduct $plan                    plan object of this subscription
  * @param string        $status                  like status class variable
  * @param boolean       $store                   true (default) if should be stored into db
  * @param int           $subscriptionTime        time of subscription
  */
 protected function createMerchandiseRecord($user_id, $plan, $status = 'R', $store = true, $subscriptionTime = null)
 {
     if ($subscriptionTime === null) {
         $subscriptionTime = cbpaidTimes::getInstance()->startTime();
     }
     $this->reset();
     $this->user_id = $user_id;
     $this->plan_id = $plan->get('id');
     $this->payment_date = $this->_db->getUtcDateTime($subscriptionTime);
     $this->getCurrencyAmount($plan);
     $this->status = $status;
     if (is_object($plan->_integrations)) {
         $this->integrations = $plan->_integrations->asJson();
     } else {
         $this->integrations = '';
     }
     if ($store) {
         $this->ip_addresses = cbpaidRequest::getIPlist();
         $this->historySetMessage($this->recordName() . ' record created');
         $this->store();
     }
     $this->_plan = $plan;
 }
	/**
	 * create a new subscription object and corresponding object in database
	 *
	 * @param int|null       $user_id                 CB user id
	 * @param cbpaidProduct  $plan                    plan object of this subscription
	 * @param array|null     $replacesSubscriptionId  array( planId, subscriptionId ) or NULL of the replaced subscription
	 * @param array|null     $parentSubId             array( PlanId, SubscriptionId ) or NULL of the parent subscription
	 * @param string         $status                  like status class variable
	 * @param boolean        $store                   true (default) if should be stored into db
	 * @param int            $subscriptionTime        time of subscription
	 */
	public function createSubscription( $user_id, &$plan, $replacesSubscriptionId = null, $parentSubId = null, $status = 'R', $store = true, $subscriptionTime = null ) {
		global $_CB_framework, $_CB_database;

		if ( $subscriptionTime === null ) {
			$subscriptionTime				=	$_CB_framework->now();
		}

		$this->reset();

		$this->user_id						=	$user_id;
		$this->plan_id						=	$plan->get( 'id' );
		if ( $replacesSubscriptionId ) {
			$this->replaces_plan			=	$replacesSubscriptionId[0];
			$this->replaces_subscription	=	$replacesSubscriptionId[1];
		}
		if ( $parentSubId ) {
			$this->parent_plan				=	$parentSubId[0];
			$this->parent_subscription		=	$parentSubId[1];
		} else {
			$this->parent_plan				=	0;
			$this->parent_subscription		=	0;
		}

		$this->subscription_date			=	date( 'Y-m-d H:i:s', $subscriptionTime );
		$this->last_renewed_date			=	$this->subscription_date;
		$this->expiry_date					=	null;
		$this->status						=	$status;
		$this->autorenew_type				=	0;			// will be changed later at payment time
		$this->autorecurring_type			=	0;			// will be changed later at payment time
		$this->regular_recurrings_total		=	$plan->get( 'recurring_max_times' );
		$this->regular_recurrings_used		=	0;
		$this->previous_recurrings_used		=	0;
		if ( is_object( $plan->_integrations ) ) {
			$this->integrations				=	$plan->_integrations->asJson();
		} else {
			$this->integrations				=	'';
		}

		if ( $store ) {
			$this->ip_addresses				=	cbpaidRequest::getIPlist();
			$this->historySetMessage( 'User subscription record created with status: ' . $status );
			if (!$this->store() ) {
				trigger_error( 'subscription store error:'.htmlspecialchars($_CB_database->getErrorMsg()), E_USER_ERROR );
			}
		}
		$this->_plan						=	$plan;
	}
 /**
  * This is the frontend or backend method used directly
  * @see cbpaidPaymentBasket::store()
  *
  * If table key (id) is NULL : inserts a new row
  * otherwise updates existing row in the database table
  *
  * Can be overridden or overloaded by the child class
  *
  * @param  boolean  $updateNulls  TRUE: null object variables are also updated, FALSE: not.
  * @return boolean                TRUE if successful otherwise FALSE
  */
 public function store($updateNulls = false)
 {
     global $_CB_framework, $_CB_database;
     // 1) check:
     if (!in_array($this->payment_status, array('Pending', 'Refunded', 'NotInitiated'))) {
         $this->setError(CBPTXT::T("This payment basket is not pending."));
         return false;
     }
     if ($this->txn_id == '') {
         $this->txn_id = 'None';
         // needed for updatePayment to generate payment record.
     }
     $paymentBasket = new cbpaidPaymentBasket($_CB_database);
     $paymentBasket->load($this->id);
     if (!$paymentBasket->gateway_account) {
         $this->setError(CBPTXT::T("This payment basket has no gateway associated so can not be paid manually."));
         return false;
     }
     $ipn = new cbpaidPaymentNotification($_CB_database);
     $ipn->bindObjectToThisObject($paymentBasket, 'id');
     $ipn->mc_currency = $this->mc_currency;
     $ipn->mc_gross = $this->mc_gross;
     if (!preg_match('/^[1-9][0-9]{3}-[01][0-9]-[0-3][0-9]/', $this->time_completed)) {
         $this->time_completed = Application::Database()->getUtcDateTime();
     }
     $paymentBasket->time_completed = $this->time_completed;
     $ipn->payment_type = $this->payment_type;
     $paymentBasket->payment_type = $this->payment_type;
     $ipn->txn_id = $this->txn_id;
     $paymentBasket->txn_id = $this->txn_id;
     $ipn->payment_status = 'Completed';
     $ipn->txn_type = 'web_accept';
     $ipn->payment_method = $this->payment_method;
     $ipn->gateway_account = $this->gateway_account;
     $ipn->log_type = 'P';
     $ipn->time_received = $_CB_database->getUtcDateTime();
     $ipn->payment_date = gmdate('H:i:s M d, Y T', $this->time_completed ? cbpaidTimes::getInstance()->strToTime($this->time_completed) : cbpaidTimes::getInstance()->startTime());
     // paypal-style				//TBD FIXME: WE SHOULD CHANGE THIS OLD DATE STYLE ONCE WITH UTC timezone inputed
     $ipn->payment_basket_id = $this->id;
     $ipn->raw_result = 'manual';
     $ipn->raw_data = '';
     $ipn->ip_addresses = cbpaidRequest::getIPlist();
     $ipn->user_id = Application::MyUser()->getUserId();
     $ipn->txn_id = $this->txn_id;
     $ipn->payment_type = $this->payment_type;
     $ipn->charset = $_CB_framework->outputCharset();
     //TBD
     /*
     		$paymentBasket->first_name	= $ipn->first_name	= cbGetParam( $_POST, 'txtBTitle' );
     		$paymentBasket->first_name		= $ipn->first_name		= cbGetParam( $_POST, 'txtBFirstName' );
     		$paymentBasket->last_name		= $ipn->last_name		= cbGetParam( $_POST, 'txtBLastName' );
     		$paymentBasket->address_street	= $ipn->address_street	= cbGetParam( $_POST, 'txtBAddr1' );
     		$paymentBasket->address_zip		= $ipn->address_zip		= cbGetParam( $_POST, 'txtBZipCode' );
     		$paymentBasket->address_city	= $ipn->address_city	= cbGetParam( $_POST, 'txtBCity' );
     		$paymentBasket->address_country	= $ipn->address_country	= cbGetParam( $_POST, 'txtBCountry' );
     		//TBD? $paymentBasket->phone	= $ipn->phone			= cbGetParam( $_POST, 'txtBTel' );
     		//TBD? $paymentBasket->fax		= $ipn->fax				= cbGetParam( $_POST, 'txtBFax' );
     		$paymentBasket->payer_email		= $ipn->payer_email		= cbGetParam( $_POST, 'txtBEmail' );
     */
     if (!$_CB_database->insertObject($ipn->getTableName(), $ipn, $ipn->getKeyName())) {
         trigger_error('store error:' . htmlspecialchars($_CB_database->getErrorMsg()), E_USER_ERROR);
         //TBD also in paypal: error code 500 !!!
     }
     $payAccount = cbpaidControllerPaychoices::getInstance()->getPayAccount($paymentBasket->gateway_account);
     if (!$payAccount) {
         $this->setError(CBPTXT::T("This payment basket's associated gateway account is not active, so can not be paid manually."));
         return false;
     }
     $payClass = $payAccount->getPayMean();
     $payClass->updatePaymentStatus($paymentBasket, 'web_accept', 'Completed', $ipn, 1, 0, 0, 'singlepayment');
     return true;
 }
	/**
	 * Logs notification
	 *
	 * @param  string                           $log_type
	 * @param  int                              $now
	 * @param  cbpaidPaymentBasket              $paymentBasket
	 * @return cbpaidPaymentNotification
	 */
	private function _logNotification( $log_type, $now, $paymentBasket )
	{
		global $_CB_database;

		$ipn = new cbpaidPaymentNotification($_CB_database);
		$ipn->payment_method	=	$this->getPayName();
		$ipn->gateway_account	=	$this->getAccountParam( 'id' );
		$ipn->log_type			=	$log_type;
		$ipn->time_received		=	date( 'Y-m-d H:i:s', $now );
		$ipn->payment_basket_id	=	$paymentBasket->id;

		$ipn->raw_data			=	'$_POST=' . var_export( $_POST, true ) . ';\n';

		$ipn->raw_result 		=	'FREE_TRIAL';
		$ipn->ip_addresses		=	cbpaidRequest::getIPlist();
		$ipn->notify_version	=	'2.1';
		$ipn->user_id			=	(int) cbGetParam( $_GET, 'user', 0 );
		$ipn->charset			=	'utf-8';
		$ipn->test_ipn			=	0;
		$ipn->first_name		=	$paymentBasket->first_name;
		$ipn->last_name			=	$paymentBasket->last_name;
		$ipn->payer_status		=	'unverified';
		$ipn->item_name			=	$paymentBasket->item_name;
		$ipn->item_number		=	$paymentBasket->item_number;
		$ipn->quantity			=	$paymentBasket->quantity;
		$ipn->custom			=	$paymentBasket->id;
		$ipn->invoice			=	$paymentBasket->invoice;
		$ipn->mc_currency		=	$paymentBasket->mc_currency;
		$ipn->tax				=	'0.00';
		$ipn->mc_gross			=	'0.00';
		$ipn->payment_status	=	'Completed';
		$ipn->payment_date		=	date( 'H:i:s M d, Y T', $now );			// paypal-style
		$ipn->payment_type		=	'Free trial';
		$ipn->txn_id			=	null;
		$ipn->txn_type			=	'web_accept';
		$ipn->recurring			=	0;

		$_CB_database->insertObject( $ipn->getTableName(), $ipn, $ipn->getKeyName() );

		return $ipn;
	}
	/**
	 * create a paymentBasket in database
	 *
	 * @param  UserTable  $user
	 * @param  float      $price
	 * @param  string     $currency
	 * @param  int        $quantity
	 * @param  string     $item_number
	 * @param  string     $item_name
	 * @param  boolean    $store         default: TRUE: store object in database, FALSE: keep in memory only
	 * @param  int        $now           unix time
	 * @param  int        $owner         basket owner (seller)
	 * @param  string     $reason        payment reason: 'N'=new subscription (default), 'R'=renewal, 'U'=update
	 */
	public function createPaymentBasket( &$user, $price, $currency, $quantity, $item_number, $item_name, $store, $now, $owner, /** @noinspection PhpUnusedParameterInspection */ $reason ) {
		global $_CB_database;

		$this->reset();

		$this->user_id			=	(int) $user->id;
		$this->owner			=	(int) $owner;
		$this->payment_status	=	'NotInitiated';
		$this->time_initiated	=	date( 'Y-m-d H:i:s', $now );
		$this->ip_addresses		=	cbpaidRequest::getIPlist();
		$this->mc_gross			=	$price;		// for now, later sum...
		$this->mc_currency		=	$currency;
		$this->quantity			=	$quantity;
		$this->item_number		=	$item_number;
		$this->item_name		=	$item_name;
		$this->setRandom_shared_secret();
		$this->_setInvoicingAddress( $user );
		if ( $store ) {
			$this->historySetMessage( 'Creating new payment basket' );
			if ( ! $this->store() ) {
				trigger_error( 'payment_basket store error:' . htmlspecialchars( $_CB_database->getErrorMsg() ), E_USER_ERROR );
			}
		}
	}
	/**
	 * Prepares AIM request
	 *
	 * @param  string				$aimRequestType	 : AUTH_CAPTURE, AUTH_ONLY, CAPTURE_ONLY, CREDIT, VOID, PRIOR_AUTH_CAPTURE
	 * @param  array				$card			 : $card['type'], $card['number'], $card['firstname'], $card['lastname'], $card['expmonth'], $card['expyear'], and optionally: $card['address'], $card['zip'], $card['country']
	 * @param  cbpaidPaymentBasket	$paymentBasket	 : WARNING: Using mc_amount3 as price as it's a subscription, instead of mc_gross.
	 * @param  boolean				$authnetSubscription   true if it is a subscription and amount is in mc_amount1 and not in mc_gross
	 * @return mixed				string of XML request 
	 */
	private function _encodeAIMPostRequest( $aimRequestType, $card, &$paymentBasket, $authnetSubscription )
	{
		$authorize_login_id			= $this->ISOtoUtf8( $this->getAccountParam( 'authorize_login_id' ) );
		$authorize_transaction_key	= $this->ISOtoUtf8( $this->getAccountParam( 'authorize_transaction_key' ) );
		$invoiceNum					= $this->ISOtoUtf8( $paymentBasket->invoice ? $paymentBasket->invoice : $paymentBasket->id );
		$refId						= $this->ISOtoUtf8( $paymentBasket->id );
		// $subscriptionName			= $this->_cbp_utf8_substr( $this->ISOtoUtf8( $paymentBasket->item_name ), 0, 20 );
		$subscriptionDescription	= $this->_cbp_utf8_substr( $this->ISOtoUtf8( $paymentBasket->item_name ), 0, 255 );
		
		//TBD Check if really needed ! $subscriptionTiming			= $this->_computeSubscriptionTiming( $paymentBasket, 'noUpfrontFirstCharge', $card );
		if ( $authnetSubscription > 1 ) {
			if ( $paymentBasket->period1 ) {
				$amount				= sprintf( '%.2f', $paymentBasket->mc_amount1 );
			} else {
				$amount				= sprintf( '%.2f', $paymentBasket->mc_amount3 );
			}
		} else {
			$amount					= sprintf( '%.2f', $paymentBasket->mc_gross );
		}
		$cardNumber					= substr( preg_replace ( '/[^0-9]+/', '', strval( $card['number'] ) ), 0, 22 );
		$cardExpirationDateMM_YYYY	= substr( sprintf( '%02d', intval( $card['expmonth'] ) ), 0, 2 ) . '-' . substr( strval( intval( $card['expyear'] ) ), 0, 4 );
		$cardCVV					= substr( preg_replace ( '/[^0-9]+/', '', strval( $card['cvv'] ) ), 0, 4 );
		if ( ! $cardCVV ) {
			$cardCVV				= '';
		}
		$firstName					= $this->_cbp_utf8_substr( $this->ISOtoUtf8( $card['firstname'] ), 0, 50 );
		$lastName					= $this->_cbp_utf8_substr( $this->ISOtoUtf8( $card['lastname'] ), 0, 50 );

		$email						= $this->_cbp_utf8_substr( $this->ISOtoUtf8( $paymentBasket->payer_email ), 0, 255 );

		$country					= ( isset( $card['country'] ) ? $this->_cbp_utf8_substr( $this->ISOtoUtf8( $card['country'] ), 0, 60 ) : null );
		$zip						= ( isset( $card['zip'] ) ? $this->_cbp_utf8_substr( $this->ISOtoUtf8( $card['zip'] ), 0, 20 ) : null );
		$address					= ( isset( $card['address'] ) ? $this->_cbp_utf8_substr( $this->ISOtoUtf8( $card['address'] ), 0, 60 ) : null );

		$ipAddressesArray			= cbpaidRequest::getIParray();
		$ipAddress					= substr( array_pop( $ipAddressesArray ), 0, 15 );			// last one is the address we get from our own (trusted) webserver
		switch ( $aimRequestType ) {
			case 'AUTH_CAPTURE':
				$authnet_values	= array
					(
						'x_version'				=> '3.1',
					 	'x_relay_response'		=> 'FALSE',
						'x_delim_data'			=> 'TRUE',
						'x_delim_char'			=> '*',
						'x_encap_char'			=> '|',
						'x_duplicate_window'	=> '28800',		// no reason to pay twice same basket => 8 hours, maximum allowed by authorize.net
						'x_recurring_billing'	=> 'NO',
					
						'x_login'				=> $authorize_login_id,
						'x_tran_key'			=> $authorize_transaction_key,
					
						'x_amount'				=> $amount,
						'x_currency_code'		=> $paymentBasket->mc_currency,
					
						'x_method'				=> 'CC',
					 	'x_card_num'			=> $cardNumber,
						'x_exp_date'			=> $cardExpirationDateMM_YYYY,
						'x_card_code'			=> $cardCVV,
					
						'x_type'				=> $aimRequestType,
					
						'x_first_name'			=> $firstName,
						'x_last_name'			=> $lastName,
						'x_email'				=> $email,
						'x_cust_id'				=> $paymentBasket->user_id,
						'x_customer_ip'			=> $ipAddress,
					/*
						'x_address'				=> '342 N. Main Street #150',
						'x_city'				=> 'Ft. Worth',
						'x_state'				=> 'TX',
						'x_zip'					=> '12345',
					*/
						'x_invoice_num'			=> $invoiceNum,
						'x_description'			=> $subscriptionDescription,
					
						'cb_custom'				=> $refId,
					);
				if ( $country !== null ) {
					$authnet_values['x_country'] =	$country;
				}
				if ( $zip !== null ) {
					$authnet_values['x_zip']	 =	$zip;
				}
				if ( $address !== null ) {
					$authnet_values['x_address'] =	$address;
				}
				break;
		
			default:
				$authnet_values					 =	array();
				break;
		}
		return $authnet_values;
	}
 /**
  * Returns default invoice country code of this user
  *
  * @param  boolean      $onlyGeoIp
  * @return string|null
  */
 public function getDefaultInvoiceCountryCode($onlyGeoIp = true)
 {
     $buyerCountryCode = cbpaidRequest::getGeoIpCountryCode();
     if ($buyerCountryCode) {
         return $buyerCountryCode;
     }
     if ($onlyGeoIp) {
         return null;
     }
     $params = cbpaidApp::settingsParams();
     if ($params->get('integration_cbsubstax_enabled') != 1) {
         return null;
     }
     return $params->get('integration_cbsubstax_system_buyer_country_iso_code2');
 }
	/**
	 * Attempts to authorize and capture a credit card for a single payment of a payment basket
	 *
	 * @param array $card                           contains type, number, firstname, lastname, expmonth, expyear, and optionally: address, zip, country
	 * @param cbpaidPaymentBasket $paymentBasket
	 * @param int $now                              unix timestamp of now
	 * @param cbpaidsubscriptionsNotification $ipn  returns the stored notification
	 * @param boolean $authnetSubscription          true if it is a subscription and amount is in mc_amount1 and not in mc_gross
	 * @return array|bool                           subscriptionId if subscription request succeeded, otherwise ARRAY( 'level' => 'spurious' or 'fatal', 'errorText', 'errorCode' => string ) of error to display
	 */
	protected function processSinglePayment( $card, $paymentBasket, $now, &$ipn, $authnetSubscription )
	{
		if ( $this->hasPaypalApi() ) {
			$countries			=	new cbpaidCountries();

			if ( $authnetSubscription > 1 ) {
				if ( $paymentBasket->period1 ) {
					$amount		=	sprintf( '%.2f', $paymentBasket->mc_amount1 );
				} else {
					$amount		=	sprintf( '%.2f', $paymentBasket->mc_amount3 );
				}
			} else {
				$amount			=	sprintf( '%.2f', $paymentBasket->mc_gross );
			}

			$ipAddresses		=	cbpaidRequest::getIParray();

			$requestParams		=	array(	'METHOD' => 'DoDirectPayment',
											'PAYMENTACTION' => 'Sale',
											'IPADDRESS' => substr( array_pop( $ipAddresses ), 0, 15 ),
											'CREDITCARDTYPE' => cbIsoUtf_substr( $card['type'], 0, 10 ),
											'ACCT' => substr( preg_replace ( '/[^0-9]+/', '', strval( $card['number'] ) ), 0, 22 ),
											'EXPDATE' => substr( sprintf( '%02d', intval( $card['expmonth'] ) ), 0, 2 ) . substr( strval( intval( $card['expyear'] ) ), 0, 4 ),
											'CVV2' => substr( preg_replace ( '/[^0-9]+/', '', strval( $card['cvv'] ) ), 0, 4 ),
											'EMAIL' => cbIsoUtf_substr( $paymentBasket->payer_email, 0, 127 ),
											'FIRSTNAME' => cbIsoUtf_substr( $card['firstname'], 0, 25 ),
											'LASTNAME' => cbIsoUtf_substr( $card['lastname'], 0, 25 ),
											'STREET' => cbIsoUtf_substr( $paymentBasket->address_street, 0, 100 ),
											'CITY' => cbIsoUtf_substr( $paymentBasket->address_city, 0, 40 ),
											'STATE' => cbIsoUtf_substr( substr( $paymentBasket->address_state, -2 ), 0, 2 ),
											'COUNTRYCODE' => $countries->countryToTwoLetters( $paymentBasket->address_country ),
											'ZIP' => cbIsoUtf_substr( $paymentBasket->address_zip, 0, 20 ),
											'AMT' => $amount,
											'CURRENCYCODE' => $paymentBasket->mc_currency,
											'DESC' => $paymentBasket->item_name,
											'CUSTOM' => $paymentBasket->id,
											'INVNUM' => $paymentBasket->invoice,
											'NOTIFYURL' => $this->getNotifyUrl( $paymentBasket )
										);

			$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 );
			}

			if ( $error || ( $status != 200 ) || ( ! $response ) ) {
				$return			=	array(	'level' => 'spurious',
											'errorText' => CBPTXT::T( "Submitted subscription payment didn't return an error but didn't complete." ) . ' ' . CBPTXT::T( 'Please contact site administrator to check error log.' ),
											'errorCode' => '8888'
										);

				$logType		=	'B';
			} else {
				if ( cbGetParam( $results, 'ACK' ) == 'Success' ) {
					$return		=	cbGetParam( $results, 'TRANSACTIONID' );

					$logType	=	'P';
				} else {
					$return		=	array(	'level' => 'fatal',
											'errorText' => cbGetParam( $results, 'L_SHORTMESSAGE0' ) . '. ' . CBPTXT::T( 'Please contact site administrator to check error log.' ),
											'errorCode' => cbGetParam( $results, 'L_ERRORCODE0' )
										);

					$logType	=	'V';
				}
			}

			$ipn				=	$this->_logNotification( $logType, $now, $paymentBasket, $card, $requestParams, $response, $results, $return );
		} else {
			$return				=	array(	'level' => 'fatal',
											'errorText' => CBPTXT::T( 'Needed Paypal API username, password and signature not set.' ) . ' ' . CBPTXT::T( 'Please contact site administrator to check error log.' ),
											'errorCode' => '8888'
										);
		}

		return $return;
	}
/**
 * Gets a comma-separated list of IP addresses taking in account the proxys on the way.
 * An array is needed because FORWARDED_FOR can be facked as well.
 *
 * @obsolete since CBSubs 2.1
 *
 * @return string of IP addresses, first one being host, and last one last proxy (except fackings)
 */
function cbpaidGetIPlist()
{
    return cbpaidRequest::getIPlist();
}
	/**
	 * Handle Paypal PDT
	 *
	 * @param  cbpaidPaymentBasket  $paymentBasket  New empty object. returning: includes the id of the payment basket of this callback (strictly verified, otherwise untouched)
	 * @param  array                $postdata       _POST data for saving edited tab content as generated with getEditTab
	 * @return string                               HTML to display if frontend, text to return to gateway if notification, FALSE if registration cancelled and ErrorMSG generated, or NULL if nothing to display
	 */
	private function handlePaypalPDT( $paymentBasket, /** @noinspection PhpUnusedParameterInspection */ $postdata )
	{
		global $_CB_framework, $_CB_database, $_GET, $_POST;

		$ret								=	null;
		// The user got redirected back from paypal with a success message:

		if ( isset( $_GET['tx'] ) && isset( $_GET['st'] ) && isset( $_GET['amt'] ) && isset( $_GET['cc'] ) ) {

			/// P D T :		Process Payment Data Transaction (PDT):

			// check if PDT not already processed:
			$pbTmp							=	new cbpaidPaymentBasket( $_CB_database );
			$paymentBasketId				=	(int) $this->_getReqParam('basket');
			if ( $paymentBasketId
				&& $pbTmp->load( (int) $paymentBasketId )
				&& ( $pbTmp->payment_status == cbGetParam( $_GET, 'st' ) )
				&& ( $pbTmp->txn_id == cbGetParam( $_GET, 'tx' ) )
				&& ( $pbTmp->shared_secret ==cbGetParam( $_GET, 'cbpid' ) ) )
			{
				// this PDT has already been treated...probably a Nth reload or bookmarked page:
				$paymentBasket->load( (int) $pbTmp->id );
			} else {
				$ipn						=	new cbpaidPaymentNotification($_CB_database);
				$ipn->payment_method		=	$this->getPayName();
				$ipn->gateway_account		=	$this->getAccountParam( 'id' );
				// done below: $ipn->log_type			= 'R';
				$ipn->time_received			=	date( 'Y-m-d H:i:s', $_CB_framework->now() );
				$ipn->raw_data				=	/* cbGetParam() not needed: we want raw info */ '$_GET=' . var_export( $_GET, true ) . ";\n";
				$ipn->raw_data				.=	/* cbGetParam() not needed: we want raw info */ '$_POST=' . var_export( $_POST, true ) . ";\n";
				$ipn->ip_addresses			=	cbpaidRequest::getIPlist();
				$ipn->user_id				=	$pbTmp->user_id;

				// post back to PayPal system to validate:
				$formvars					=	array(	'cmd'	=> '_notify-synch',
														'tx'	=> cbGetParam( $_REQUEST, 'tx', '' ),
														'at'	=> trim($this->getAccountParam('paypal_identity_token'))
													 );
				$results					=	null;
				$status						=	null;
				$error						=	$this->_httpsRequest( $this->_paypalUrl() . '/cgi-bin/webscr', $formvars, 30, $results, $status, 'post', 'normal', '*/*', true, 443, '', '', true, null );
				$transaction_info			=	urldecode($results);			//FIXME: urldecode is done below already!

				if ( $error || ( $status != 200 ) ) {
					$ipn->raw_result		=	'COMMUNICATION ERROR';
//					$ipn->raw_data = 'Error: '. $error . ' Status: ' . $status . ' Transaction info: ' . $transaction_info;
					$ipn->raw_data			.=	'$error=\''. $error . "';\n";
					$ipn->raw_data			.=	'$status=\'' . $status . "';\n";
					$ipn->raw_data			.=	'$formvars=' . var_export( $formvars, true ) . ";\n";
					$ipn->raw_data			.=	'$transaction_info=\'' . $transaction_info . "';\n";
					$ipn->log_type			=	'E';
					$ipn->time_received		=	date( 'Y-m-d H:i:s', $_CB_framework->now() );
					$_CB_database->insertObject( $ipn->getTableName(), $ipn, $ipn->getKeyName() );

					$this->_setLogErrorMSG( 3, $ipn, 'Paypal: Error at notification received: could not reach Paypal gateway for notification check at ' . $this->_paypalUrl() . '. ' . $ipn->raw_result, null );
					$this->_setErrorMSG( sprintf( CBPTXT::T("Sorry no response for your payment from payment server (error %s). Please check your email and status later."), $error ) );
					$ret					=	false;
				} else {
					// echo $transaction_info;
					$input = explode("\n", $transaction_info);
					foreach ($input as $k => $in) {
						$input[$k]			=	trim( $in, "\n\r" );
					}
					$resultMessage = array_shift( $input );
					$output					=	array();
					foreach ($input as $in) {
						$posEqualSign		=	strpos($in, '=');
						if ($posEqualSign === false) {
							$output[]		=	$in;
						} else {
							$output[substr($in,0,$posEqualSign)]	=	substr($in, $posEqualSign+1);
						}
					}
					if ( isset( $output['charset'] ) && ( $resultMessage == 'SUCCESS' ) ) {
						if ( strtolower( $output['charset'] ) != strtolower( $_CB_framework->outputCharset() ) ) {
							foreach ($output as $k => $v ) {
								$output[$k]		=	$this->_charsetConv( $v, $output['charset'], $_CB_framework->outputCharset() );
							}
							$output['charset']	=	$_CB_framework->outputCharset();
						}			
					}
					$ipn->bind( $output );
					$ipn->raw_result 		=	$resultMessage;
					$ipn->raw_data			.=	'$transaction_info=\'' . $transaction_info . "';\n";
					$ipn->raw_data			.=	'$PDT_RESULT=' . var_export( $output, true ) . ";\n";
					$ipn->payment_basket_id	=	(int) $ipn->custom;
					/*
					if(!$_CB_database->updateObject( $ipn->_tbl, $ipn, $ipn->_tbl_key, false)) {
						echo 'update error:'.htmlspecialchars($_CB_database->stderr(true))."\n";
						exit();
					}
					*/
	
					if ( $resultMessage == 'SUCCESS' ) {
						$ipn->log_type							= 'R';
						$_CB_database->insertObject( $ipn->getTableName(), $ipn, $ipn->getKeyName() );

						$paymentBasketId						=	(int) $ipn->custom;
						$exists									=	$paymentBasket->load( (int) $paymentBasketId );
						if ( $exists ) {
							$this->_fixPayPalIpnBugs( $ipn, $paymentBasket );
							$noFraudCheckResult					=	$this->_checkNotPayPalFraud( $ipn, $paymentBasket, cbGetParam( $_REQUEST, 'cbpid', '' ) );
							if ( $noFraudCheckResult === true ) {
								$autorecurring_type				=	( in_array( $ipn->txn_type, array( 'subscr_payment', 'subscr_signup', 'subscr_modify', 'subscr_eot', 'subscr_cancel', 'subscr_failed' ) ) ? 2 : 0 );
								$paypalUserChoicePossible		=	( ( $this->getAccountParam( 'enabled', 0 ) == 3 ) && ( $paymentBasket->isAnyAutoRecurring() == 2 ) );
								$autorenew_type					=	( $autorecurring_type ? ( $paypalUserChoicePossible ? 1 : 2 ) : 0 );
								$this->_bindIpnToBasket( $ipn, $paymentBasket );
								$paymentBasket->payment_method	=	$this->getPayName();
								$paymentBasket->gateway_account	=	$this->getAccountParam( 'id' );
								$this->updatePaymentStatus( $paymentBasket, $ipn->txn_type, $ipn->payment_status, $ipn, 1, $autorecurring_type, $autorenew_type, false );

							} else {
								$this->_setLogErrorMSG( 3, $ipn, 'Received back from paypal: ' . var_export( $ipn, true ), CBPTXT::T("Payment notification mismatch: ") . $noFraudCheckResult . '.' );
								$ret = false;
								//TBD: update notification record !
								$ipn->log_type					=	'G';		// PDT FRAUD detected
								$ipn->raw_result				=	$noFraudCheckResult;
								$_CB_database->updateObject( $ipn->getTableName(), $ipn, $ipn->getKeyName(), false);
							}
						}
					} elseif ( $resultMessage == 'FAIL' ) {
						$ipn->log_type							= 'L';
						$_CB_database->insertObject( $ipn->getTableName(), $ipn, $ipn->getKeyName() );

						$this->_setLogErrorMSG( 3, $ipn, 'Paypal: Error: Received FAIL result message from Paypal', CBPTXT::T("Sorry your payment has not been processed. Transaction result:")
							. $transaction_info
							. '. ' . CBPTXT::T("Please try again and notify system administrator.") );
						$ret = false;
					} else {
						$ipn->log_type							= 'N';
						$_CB_database->insertObject( $ipn->getTableName(), $ipn, $ipn->getKeyName() );

						$this->_setLogErrorMSG( 3, $ipn, 'Paypal: Error: Received following unknown result message from Paypal: ' . $resultMessage, CBPTXT::T("Sorry no response for your payment. Please check your email and status later.") );
						$ret = false;
					}
				}
			}
		} else {
			// result=success but not a PDT from paypal:

			// it could be a subscription with a free trial period: in that case, as there is no initial transaction, we get returned without txn_id.
			// we must either guess that user subscribed (if he is allowed for free trials, or must wait for IPN:

			$paymentBasketId											=	(int) $this->_getReqParam( 'basket' );
			if ( $paymentBasketId ) {
				if ( $paymentBasket->load( (int) $paymentBasketId ) ) {
					$cbpid												=	cbGetParam( $_REQUEST, 'cbpid', '' );
					if ( $cbpid == $paymentBasket->shared_secret ) {
						$enable_paypal									=	$this->getAccountParam( 'enabled', 0 );
						$isAnyAutoRecurring								=	$paymentBasket->isAnyAutoRecurring();
						$pay1subscribe2									=	$this->_getPaySubscribePossibilities( $enable_paypal, $paymentBasket );
						if ( $isAnyAutoRecurring && ( ( $pay1subscribe2 & 0x2 ) != 0 ) && $paymentBasket->period1 ) {
							// Free first period: Wait for IPN for 20 times 1 second:
							for ( $i = 0; $i < 20; $i++ ) {
								if ( $paymentBasket->load( (int) $paymentBasketId ) ) {
									if ( $paymentBasket->payment_status == 'Completed' ) {
										break;
									}
								} else {
									break;
								}
								sleep( 1 );
							}
							if ( $paymentBasket->payment_status != 'Completed' ) {
								if ( ( $isAnyAutoRecurring == 1 ) || ( ( $isAnyAutoRecurring == 2 ) && ( $paymentBasket->mc_amount1 != 0 ) ) )  {
									// 1: forced subscription: error if no IPN came to update payment basket:
									// 2: not forced subscription but no free initial value: we really need IPN to know status:
										$this->_setErrorMSG(CBPTXT::T("Sorry, payment has not been confirmed by Paypal (no IPN received). IPN must be enabled for auto-recurring payment subscriptions."));
								} else {
									// user-choice: no need to wait for payment basket completed to activate subscriptions:

									$ipn								=	new cbpaidPaymentNotification($_CB_database);
									$ipn->payment_method				=	$this->getPayName();
									$ipn->gateway_account				=	$this->getAccountParam( 'id' );
									$ipn->log_type						=	'S';
									$ipn->time_received					=	date( 'Y-m-d H:i:s', $_CB_framework->now() );
									$ipn->raw_data						=	/* cbGetParam() not needed: we want raw info */ '$_GET=' . var_export( $_GET, true ) . ";\n";
									$ipn->raw_data						.=	/* cbGetParam() not needed: we want raw info */ '$_POST=' . var_export( $_POST, true ) . ";\n";
									$ipn->ip_addresses					=	cbpaidRequest::getIPlist();
									$ipn->payment_basket_id				=	$paymentBasket->id;
									$ipn->user_id						=	$paymentBasket->user_id;
									$_CB_database->insertObject( $ipn->getTableName(), $ipn, $ipn->getKeyName() );

									if ( $isAnyAutoRecurring == 2 )  {
										$autorecurring_type				=	2;
										$paypalUserChoicePossible		=	( ( $this->getAccountParam( 'enabled', 0 ) == 3 ) && ( $paymentBasket->isAnyAutoRecurring() == 2 ) );
										$autorenew_type					=	( $autorecurring_type ? ( $paypalUserChoicePossible ? 1 : 2 ) : 0 );
									} else {
										$autorecurring_type				=	0;
										$autorenew_type					=	0;
									}
									$paymentBasket->payment_method		=	$this->getPayName();
									$paymentBasket->gateway_account		=	$this->getAccountParam( 'id' );

									$this->updatePaymentStatus( $paymentBasket, 'web_accept', 'Completed', $ipn, 1, $autorecurring_type, $autorenew_type, false );
								}
							}
						}
					}
				}
			}


		}
		return $ret;
	}