Saturday, July 26, 2008

PHP class for Payflow Pro transactions

The Payflow Pro PHP sample code was a bit messy, so I decided to wrap it in a nice object to make the entire transaction process very easy. This class will be included as part of the FUSE PHP framework, but I also created a standalone version below:

PayFlowTransaction.class.php:

class PayFlowTransaction {

const HTTP_RESPONSE_OK = 200;
const KEY_MAP_ARRAY = 'map';

public $data;
public $headers = array();
public $gateway_retries = 3;
public $gateway_retry_wait = 5; //seconds
public $environment = 'test';

public $vps_timeout = 45;
public $curl_timeout = 90;

public $gateway_url_live = 'https://payflowpro.paypal.com';
public $gateway_url_devel = 'https://pilot-payflowpro.paypal.com';


public $avs_addr_required = 0;
public $avs_zip_required = 0;
public $cvv2_required = 0;
public $fraud_protection = false;

public $raw_response;
//public $response;
public $response_arr = array();

public $txn_successful = null;
public $raw_result;

public $debug = false;

public function __construct() {

$this->load_config();


}

public function load_config() {

if ( defined('PAYFLOWPRO_USER') ) {
$this->data['USER'] = constant('PAYFLOWPRO_USER');
}

if ( defined('PAYFLOWPRO_PWD') ) {
$this->data['PWD'] = constant('PAYFLOWPRO_PWD');
}

if ( defined('PAYFLOWPRO_PARTNER') ) {
$this->data['PARTNER'] = constant('PAYFLOWPRO_PARTNER');
}

if ( defined('PAYFLOWPRO_VENDOR') ) {
$this->data['VENDOR'] = constant('PAYFLOWPRO_VENDOR');
}
else {
if ( isset($this->data['USER']) ) {
$this->data['VENDOR'] = $this->data['USER'];
}
else {
$this->data['VENDOR'] = null;
}
}

}

public function __set( $key, $val ) {

$this->data[$key] = $val;

}

public function __get( $key ) {

if ( isset($this->data[$key]) ) {
return $this->data[$key];
}

return null;
}

public function get_gateway_url() {

if ( strtolower($this->environment) == 'live' ) {
return $this->gateway_url_live;
}
else {
return $this->gateway_url_devel;
}

}

public function get_data_string() {

$query = array();

if ( !isset($this->data['VENDOR']) || !$this->data['VENDOR'] ) {
$this->data['VENDOR'] = $this->data['USER'];
}


foreach ( $this->data as $key => $value) {

if ( $this->debug ) {
echo "{$key} = {$value}
";
}

$query[] = strtoupper($key) . '[' .strlen($value).']='.$value;
}

return implode('&', $query);

}

public function before_send_transaction() {

$this->txn_successful = false;
$this->raw_response = null; //reset raw result
$this->response_arr = array();
}

public function reset() {

$this->txn_successful = null;
$this->raw_response = null; //reset raw result
$this->response_arr = array();
$this->data = array();
$this->load_config();
}


public function send_transaction() {

try {

$this->before_send_transaction();

$data_string = $this->get_data_string();

$headers[] = "Content-Type: text/namevalue"; //or text/xml if using XMLPay.
$headers[] = "Content-Length: " . strlen ($data_string); // Length of data to be passed
$headers[] = "X-VPS-Timeout: {$this->vps_timeout}";
$headers[] = "X-VPS-Request-ID:" . uniqid(rand(), true);
$headers[] = "X-VPS-VIT-Client-Type: PHP/cURL"; // What you are using

$headers = array_merge( $headers, $this->headers );

if ( $this->debug ) {
echo __METHOD__ . ' Sending: ' . $data_string . '
';
}

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->get_gateway_url() );
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
curl_setopt($ch, CURLOPT_HEADER, 1); // tells curl to include headers in response
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // return into a variable
curl_setopt($ch, CURLOPT_TIMEOUT, 90); // times out after 90 secs
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); // this line makes it work under https
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string); //adding POST data
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); //verifies ssl certificate
curl_setopt($ch, CURLOPT_FORBID_REUSE, TRUE); //forces closure of connection when done
curl_setopt($ch, CURLOPT_POST, 1); //data sent as POST

$i = 0;

while ($i++ <= $this->gateway_retries) {

$result = curl_exec($ch);
$headers = curl_getinfo($ch);

if (array_key_exists('http_code', $headers) && $headers['http_code'] != self::HTTP_RESPONSE_OK) {
sleep($this->gateway_retry_wait); // Let's wait to see if its a temporary network issue.
}
else {
// we got a good response, drop out of loop.
break;
}
}

if ( !array_key_exists('http_code', $headers) || $headers['http_code'] != self::HTTP_RESPONSE_OK ) {
throw new InvalidResponseCodeException;
}

$this->raw_response = $result;

$result = strstr($result, "RESULT");
$ret = array();

while( strlen($result) > 0 ){

$keypos = strpos($result,'=');
$keyval = substr($result,0,$keypos);

// value
$valuepos = strpos($result,'&') ? strpos($result,'&'): strlen($result);
$valval = substr($result,$keypos+1,$valuepos-$keypos-1);

// decoding the respose
$ret[$keyval] = $valval;

$result = substr($result, $valuepos+1, strlen($result) );
}

return $ret;
}
catch( Exception $e ) {
@curl_close($ch);
throw $e;
}
}

public function response_handler( $response_arr ) {

try {
$result_code = $response_arr['RESULT']; // get the result code to validate.

if ( $this->debug ) {
echo __METHOD__ . ' response=' . print_r( $response_arr, true) . '
';
echo __METHOD__ . ' RESULT=' . $result_code . '
';
}

if ( $result_code == 0 ) {

//
// Even on zero, still check AVS
//

if ( $this->avs_addr_required ) {
$err_msg = "Your billing (street) information does not match.";

if ( isset($response_arr['AVSADDR'])) {
if ($response_arr['AVSADDR'] != "Y") {
throw new AVSException( $err_msg );
}
}
else {
if ( $this->avs_addr_required == 2 ) {
throw new AVSException( $err_msg );
}
}
}

if ( $this->avs_zip_required ) {

$err_msg = "Your billing (zip) information does not match. Please re-enter.";

if (isset($nvpArray['AVSZIP'])) {
if ($nvpArray['AVSZIP'] != "Y") {
throw new AVSException( $err_msg );
}
}
else {
if ( $this->avs_zip_required == 2 ) {
throw new AVSException( $err_msg );
}

}
}

if ( $this->require_cvv2_match ) {

$err_msg = "Your card code is invalid. Please re-enter.";

if ( array_key_exists('CVV2MATCH', $response_arr) ) {
if ($response_arr['CVV2MATCH'] != "Y") {
throw new CVV2Exception( $err_msg );
}
}
else {
if ( $this->require_cvv2_match == 2 ) {
throw new CVV2Exception( $err_msg );
}
}
}

//
// Return code was 0 and no AVS exceptions raised
//
$this->txn_successful = true;

parse_str($this->raw_response, $this->response_arr);
return $this->response_arr;
}
else if ($result_code == 1 || $result_code == 26) {
throw new InvalidCredentialsException( "Invalid API Credentials" );
}
else if ($result_code == 12) {
// Hard decline from bank.
throw new TransactionDataException( "Your transaction was declined." );
}
else if ($result_code == 13) {
// Voice authorization required.
throw new TransactionDataException ("Your Transaction is pending. Contact Customer Service to complete your order.");
}
else if ($result_code == 23 || $result_code == 24) {
// Issue with credit card number or expiration date.
$msg = 'Invalid credit card information: ' . $response_arr['RESPMSG'];
throw new TransactionDataException ($msg);
}

// Using the Fraud Protection Service.
// This portion of code would be is you are using the Fraud Protection Service, this is for US merchants only.
if ( $this->fraud_protection ) {

if ($result_code == 125) {
// 125 = Fraud Filters set to Decline.
throw new FraudProtectionException ( "Your Transaction has been declined. Contact Customer Service to place your order." );
}
else if ($result_code == 126) {
throw new FraudProtectionException ( "Your Transaction is Under Review. We will notify you via e-mail if accepted." );
}
else if ($result_code == 127) {
throw new FraudProtectionException ( "Your Transaction is Under Review. We will notify you via e-mail if accepted." );
}
}

//
// Throw generic response
//
throw new FuseException( $response_arr['RESPMSG'] );


}
catch( Exception $e ) {
throw $e;
}

}

public function process() {

try {
return $this->response_handler($this->send_transaction());
}
catch( Exception $e ) {
throw $e;
}

}

public function apply_associative_array( $arr, $options = array() ) {

try {

$map_array = array();

if ( isset($options[self::KEY_MAP_ARRAY]) ) {
$map_array = $options[self::KEY_MAP_ARRAY];
}

foreach( $arr as $cur_key => $val ) {

if( isset($map_array[$cur_key]) ) {
$cur_key = $map_array[$cur_key];
}
else {
if ( isset($options['require_map']) && $options['require_map'] ) {
continue;
}
}

$this->data[strtoupper($cur_key)] = $val;

}
}
catch( Exception $e ) {
throw $e;
}

}


}


class InvalidCredentialsException extends Exception {

}

class GatewayException extends Exception {

}

class InvalidResponseCodeException extends GatewayException {

}


class TransactionDataException extends Exception {

}

class AVSException extends TransactionDataException {

}

class CVV2Exception extends TransactionDataException {

}

class FraudProtectionException extends Exception {

}




Usage:


try {

require_once('PayFlowTransaction.class.php');//assumes it's in the current dir

$txn = new PayflowTransaction();

//
//these are provided by your payflow reseller
//
$txn->PARTNER = 'yourpartnername';
$txn->USER = 'yourusername';
$txn->PWD= 'yourpassword';
$txn->VENDOR = $txn->USER; //or your vendor name

//
// transaction information
//

$txn->TENDER = 'C'; //sets to a cc transaction
$txn->ACCT = '4111111111111111'; //cc number
$txn->TRXTYPE = 'S'; //txn type: sale
$txn->AMT = 1.00; //amount: 1 dollar
$txn->EXPDATE='0210'; //4 digit expiration date


$txn->FIRSTNAME = 'Joe';
$txn->LASTNAME = 'Junior Shabadu';
$txn->STREET = '123 mystreet';
$txn->CITY = 'Philadelphia';
$txn->STATE = 'PA';
$txn->ZIP = '19115';
$txn->COUNTRY = 'US';

//$txn->debug = true; //uncomment to see debugging information
//$txn->avs_addr_required = 1; //set to 1 to enable AVS address checking, 2 to force "Y" response
//$txn->avs_zip_required = 1; //set to 1 to enable AVS zip code checking, 2 to force "Y" response
//$txn->cvv2_required = 1; //set to 1 to enable cvv2 checking, 2 to force "Y" response
//$txn->fraud_protection = true; //uncomment to enable fraud protection

$txn->process();

echo "success: " . $txn->txn_successful;
echo "response was: " . print_r( $txn->response_arr, true );

}
catch( TransactionDataException $tde ) {
echo 'bad transaction data ' . $tde->getMessage();
}
catch( InvalidCredentialsException $e ) {
echo 'Invalid credentials';
}
catch( InvalidResponseCodeException $irc ) {
echo 'bad response code: ' . $irc->getMessage();
}
catch( AVSException $avse ) {
echo 'AVS error: ' . $avse->getMessage();
}
catch( CVV2Exception $cvve ) {
echo 'CVV2 error: ' . $cvve->getMessage();
}
catch( FraudProtectionException $fpe ) {
echo 'Fraud Protection error: ' . $fpe->getMessage();
}
catch( Exception $e ) {
echo $e->getMessage();
}





You will probably want to implement better error checking than what I did in the example. However, you'll still want to display TransactionDataException messages back to the user, because they're usually invalid expiration dates or something similar.

18 comments:

Ben said...

very nice! Thank you for sharing.

Richard said...

Thanks Jim, you saved me a few hours work!

Anonymous said...

酒店經紀PRETTY GIRL 台北酒店經紀人 ,禮服店 酒店兼差PRETTY GIRL酒店公關 酒店小姐 彩色爆米花酒店兼職,酒店工作 彩色爆米花酒店經紀, 酒店上班,酒店工作 PRETTY GIRL酒店喝酒酒店上班 彩色爆米花台北酒店酒店小姐 PRETTY GIRL酒店上班酒店打工PRETTY GIRL酒店打工酒店經紀 彩色爆米花

Anonymous said...

I think there may be a problem with your code as you cannot run the script with and just gives you a blank screen.

Sudath said...

It works fine in localhost for both live and pilot environment. But on live server it shows only blank page. No transaction happened. Any idea?

Anonymous said...

If you cut and pasted the sample code from the scrollable inner frames, be sure you get all of it. On my machine/browser the code sample was larger than my copy buffer could hold in one go.

yasar said...

thanks for sharing. i used this class to upgrade existing client version 3.0.7 to 4.3 .

The only problem i run into was the "$txn->response_arr".
var_dump($txn->response_arr) :

Array ( [HTTP/1_1_200_OK Connection:_close Server:_VPS-3_033_00 X-VPS-Request-ID:_8550415154a9c0d8c810712_91056448 Date:_Mon,_31_Aug_2009_17:51:08_GMT Content-type:_text/namevalue Content-length:_____86 RESULT] => 0 [PNREF] => V78N1E12E08B [RESPMSG] => Approved [AUTHCODE] => 022794 [AVSADDR] => Y [AVSZIP] => Y [IAVS] => N )


The RESULT key is combined with the header response from cURL. I commented out the line#280 (parse_str($this->raw_response, $this->response_arr);) and added $this->response_arr = $response_arr;.


I needed this change to keep remaining of the existing code intact.

Thanks again for sharing..

Wow Gold said...

WOW GOLD from randyrun. Most cheapest wow gold supplier.More than 10,000 online satisfied customers bears to the fact that we are genuine and fastest wowgold provider!

酒店上班請找艾葳 said...

艾葳酒店經紀公司提供專業的酒店經紀, 酒店上班小姐,八大行業,酒店兼職,傳播妹,或者想要打工兼差打工,兼差,八大行業,酒店兼職,想去酒店上班, 日式酒店,便服店,制服酒店,ktv酒店,禮服店,整天穿得水水漂漂的,還是想去制服店日領上班小姐,水水們如果想要擁有打工工作、晚上兼差工作兼差打工假日兼職兼職工作酒店兼差兼差打工兼差日領工作晚上兼差工作酒店工作酒店上班酒店打工兼職兼差兼差工作酒店上班等,想了解酒店相關工作特種行業內容,想兼職工作日領假日兼職兼差打工、或晚班兼職想擁有鋼琴酒吧又有保障的工作嗎???又可以現領請找專業又有保障的艾葳酒店經紀公司!

艾葳酒店經紀是合法的公司工作環境高雅時尚,無業績壓力,無脫秀無喝酒壓力,高層次會員制客源,工作輕鬆,可日領現領
一般的酒店經紀只會在水水們第一次上班和領薪水時出現而已,對水水們的上班安全一點保障都沒有!艾葳酒店經紀公司的水水們上班時全程媽咪作陪,不需擔心!只提供最優質的酒店上班,酒店上班,酒店打工環境、上班條件給水水們。心動嗎!? 趕快來填寫你的酒店上班履歷表

水水們妳有缺現領、想要兼職、有缺錢的煩腦嗎?想到日本留學缺錢嗎?妳是傳播妹??想要擁有高時薪又輕鬆的賺錢,酒店和,假日打工,假日兼職賺錢的機會嗎??想實現夢想卻又缺錢沒錢嗎!??
艾葳酒店台北酒店經紀招兵買馬!!徵專業的酒店打工,想要去酒店的水水,想要短期日領,酒店日領,禮服酒店,制服店,酒店經紀,ktv酒店,便服店,酒店工作,禮服店,酒店小姐,酒店經紀人,
等相關服務 幫您快速的實現您的夢想~!!

Anonymous said...

Thanks a lot for the given code. In the Environment variable you have defined as "Test". For making live what i need to provide here

Brian H. said...

This is awesome! Thanks for sharing this, Jim!

Anonymous said...

hai
i m using this script. But i m getting error of type credential error.Plz help me to sort out this problem. how can i validate user's personal information like city name, country name etc.
Thanks

qishaya said...

Milan create week ended last night with donatella christian louboutin london disregard presenting the ending show of the autumn christian louboutin online frost 2003 italian collections.Donatella, the creative chief of christian boots the house her delayed brother founded, delivered a collection that was, christian louboutin uk as they say, very christian louboutin shoes.First up was the versus limit, louboutin boots the cheaper christian louboutin line. Girls stomped christian louboutin 2010 out with backcombed tresses bearing turquoise leather trousers, christian louboutin uk sale patchwork pullover and blonde fur sliced into stoles and active jackets. christian boots uk But this aggressive hell’s angels look almost seemed a caricature of the christian louboutin boots christian louboutin boots comfort. louboutin sandals Next up was gianni christian louboutin, buy christian louboutin the main collection

咪摩兔 said...

信貸
代書
融資
週轉
汽車借款

michaelsoftprofessional said...

Jim, been using this script for quite some time now. However now out of the blue i am getting: Fatal error: Class 'FuseException' not found in /pathhere/PayFlowTransaction.class.php on line 329

Any ideas.

michaelsoftprofessional said...
This comment has been removed by the author.
Anonymous said...

You rock! Thanks for sharing this!

Anonymous said...

Class 'FuseException' not found, this is the error which I encounter??

Plz, help me with this??