_mode = $mode; $this->_paymentProcessor = $paymentProcessor; $this->_processorName = ts('Payflow Pro'); } /* * This function sends request and receives response from * the processor. It is the main function for processing on-server * credit card transactions */ /** * This function collects all the information from a web/api form and invokes * the relevant payment processor specific functions to perform the transaction * * @param array $params * Assoc array of input parameters for this transaction. * * @return array * the result in an nice formatted array (or an error object) * @abstract */ public function doDirectPayment(&$params) { if (!defined('CURLOPT_SSLCERT')) { CRM_Core_Error::fatal(ts('Payflow Pro requires curl with SSL support')); } /* * define variables for connecting with the gateway */ // Are you using the Payflow Fraud Protection Service? // Default is YES, change to NO or blank if not. //This has not been investigated as part of writing this payment processor $fraud = 'NO'; //if you have not set up a separate user account the vendor name is used as the username if (!$this->_paymentProcessor['subject']) { $user = $this->_paymentProcessor['user_name']; } else { $user = $this->_paymentProcessor['subject']; } // ideally this id would be passed through into this class as // part of the paymentProcessor //object with the other variables. It seems inefficient to re-query to get it. //$params['processor_id'] = CRM_Core_DAO::getFieldValue( // 'CRM_Contribute_DAO_ContributionP //age',$params['contributionPageID'], 'payment_processor_id' ); /* *Create the array of variables to be sent to the processor from the $params array * passed into this function * */ $payflow_query_array = array( 'USER' => $user, 'VENDOR' => $this->_paymentProcessor['user_name'], 'PARTNER' => $this->_paymentProcessor['signature'], 'PWD' => $this->_paymentProcessor['password'], // C - Direct Payment using credit card 'TENDER' => 'C', // A - Authorization, S - Sale 'TRXTYPE' => 'S', 'ACCT' => urlencode($params['credit_card_number']), 'CVV2' => $params['cvv2'], 'EXPDATE' => urlencode(sprintf('%02d', (int) $params['month']) . substr($params['year'], 2, 2)), 'ACCTTYPE' => urlencode($params['credit_card_type']), 'AMT' => urlencode($this->getAmount($params)), 'CURRENCY' => urlencode($params['currency']), 'FIRSTNAME' => $params['billing_first_name'], //credit card name 'LASTNAME' => $params['billing_last_name'], //credit card name 'STREET' => $params['street_address'], 'CITY' => urlencode($params['city']), 'STATE' => urlencode($params['state_province']), 'ZIP' => urlencode($params['postal_code']), 'COUNTRY' => urlencode($params['country']), 'EMAIL' => $params['email'], 'CUSTIP' => urlencode($params['ip_address']), 'COMMENT1' => urlencode($params['contributionType_accounting_code']), 'COMMENT2' => $mode, 'INVNUM' => urlencode($params['invoiceID']), 'ORDERDESC' => urlencode($params['description']), 'VERBOSITY' => 'MEDIUM', 'BILLTOCOUNTRY' => urlencode($params['country']), ); if ($params['installments'] == 1) { $params['is_recur'] = FALSE; } if ($params['is_recur'] == TRUE) { $payflow_query_array['TRXTYPE'] = 'R'; $payflow_query_array['OPTIONALTRX'] = 'S'; $payflow_query_array['OPTIONALTRXAMT'] = $this->getAmount($params); //Amount of the initial Transaction. Required $payflow_query_array['ACTION'] = 'A'; //A for add recurring (M-modify,C-cancel,R-reactivate,I-inquiry,P-payment $payflow_query_array['PROFILENAME'] = urlencode('RegularContribution'); //A for add recurring (M-modify,C-cancel,R-reactivate,I-inquiry,P-payment if ($params['installments'] > 0) { $payflow_query_array['TERM'] = $params['installments'] - 1; //ie. in addition to the one happening with this transaction } // $payflow_query_array['COMPANYNAME'] // $payflow_query_array['DESC'] = not set yet Optional // description of the goods or //services being purchased. //This parameter applies only for ACH_CCD accounts. // The // $payflow_query_array['MAXFAILPAYMENTS'] = 0; // number of payment periods (as s //pecified by PAYPERIOD) for which the transaction is allowed //to fail before PayPal cancels a profile. the default // value of 0 (zero) specifies no //limit. Retry //attempts occur until the term is complete. // $payflow_query_array['RETRYNUMDAYS'] = (not set as can't assume business rule switch ($params['frequency_unit']) { case '1 week': $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m"), date("d") + 7, date("Y") ); $params['end_date'] = mktime(0, 0, 0, date("m"), date("d") + (7 * $payflow_query_array['TERM']), date("Y") ); $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']); $payflow_query_array['PAYPERIOD'] = "WEEK"; $params['frequency_unit'] = "week"; $params['frequency_interval'] = 1; break; case '2 weeks': $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m"), date("d") + 14, date("Y")); $params['end_date'] = mktime(0, 0, 0, date("m"), date("d") + (14 * $payflow_query_array['TERM']), date("Y ") ); $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']); $payflow_query_array['PAYPERIOD'] = "BIWK"; $params['frequency_unit'] = "week"; $params['frequency_interval'] = 2; break; case '4 weeks': $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m"), date("d") + 28, date("Y") ); $params['end_date'] = mktime(0, 0, 0, date("m"), date("d") + (28 * $payflow_query_array['TERM']), date("Y") ); $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']); $payflow_query_array['PAYPERIOD'] = "FRWK"; $params['frequency_unit'] = "week"; $params['frequency_interval'] = 4; break; case '1 month': $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m") + 1, date("d"), date("Y") ); $params['end_date'] = mktime(0, 0, 0, date("m") + (1 * $payflow_query_array['TERM']), date("d"), date("Y") ); $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']); $payflow_query_array['PAYPERIOD'] = "MONT"; $params['frequency_unit'] = "month"; $params['frequency_interval'] = 1; break; case '3 months': $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m") + 3, date("d"), date("Y") ); $params['end_date'] = mktime(0, 0, 0, date("m") + (3 * $payflow_query_array['TERM']), date("d"), date("Y") ); $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']); $payflow_query_array['PAYPERIOD'] = "QTER"; $params['frequency_unit'] = "month"; $params['frequency_interval'] = 3; break; case '6 months': $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m") + 6, date("d"), date("Y") ); $params['end_date'] = mktime(0, 0, 0, date("m") + (6 * $payflow_query_array['TERM']), date("d"), date("Y") ); $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date'] ); $payflow_query_array['PAYPERIOD'] = "SMYR"; $params['frequency_unit'] = "month"; $params['frequency_interval'] = 6; break; case '1 year': $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m"), date("d"), date("Y") + 1 ); $params['end_date'] = mktime(0, 0, 0, date("m"), date("d"), date("Y") + (1 * $payflow_query_array['TEM']) ); $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']); $payflow_query_array['PAYPERIOD'] = "YEAR"; $params['frequency_unit'] = "year"; $params['frequency_interval'] = 1; break; } } CRM_Utils_Hook::alterPaymentProcessorParams($this, $params, $payflow_query_array); $payflow_query = $this->convert_to_nvp($payflow_query_array); /* * Check to see if we have a duplicate before we send */ if ($this->checkDupe($params['invoiceID'], CRM_Utils_Array::value('contributionID', $params))) { return self::errorExit(9003, 'It appears that this transaction is a duplicate. Have you already submitted the form once? If so there may have been a connection problem. Check your email for a receipt. If you do not receive a receipt within 2 hours you can try your transaction again. If you continue to have problems please contact the site administrator.'); } // ie. url at payment processor to submit to. $submiturl = $this->_paymentProcessor['url_site']; $responseData = self::submit_transaction($submiturl, $payflow_query); /* * Payment successfully sent to gateway - process the response now */ $result = strstr($responseData, "RESULT"); if (empty($result)) { return self::errorExit(9016, "No RESULT code from PayPal."); } $nvpArray = array(); while (strlen($result)) { // name $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 $nvpArray[$keyval] = $valval; $result = substr($result, $valuepos + 1, strlen($result)); } // get the result code to validate. $result_code = $nvpArray['RESULT']; /*debug echo "

Params array


"; print_r($params); echo "


"; echo "

Values to Payment Processor


"; print_r($payflow_query_array); echo "


"; echo "

Results from Payment Processor


"; print_r($nvpArray); echo "


"; */ switch ($result_code) { case 0: /******************************************************* * Success ! * This is a successful transaction. Payflow Pro does return further information * about transactions to help you identify fraud including whether they pass * the cvv check, the avs check. This is stored in * CiviCRM as part of the transact * but not further processing is done. Business rules would need to be defined *******************************************************/ $params['trxn_id'] = $nvpArray['PNREF'] . $nvpArray['TRXPNREF']; //'trxn_id' is varchar(255) field. returned value is length 12 $params['trxn_result_code'] = $nvpArray['AUTHCODE'] . "-Cvv2:" . $nvpArray['CVV2MATCH'] . "-avs:" . $nvpArray['AVSADDR']; if ($params['is_recur'] == TRUE) { $params['recur_trxn_id'] = $nvpArray['PROFILEID']; //'trxn_id' is varchar(255) field. returned value is length 12 } return $params; case 1: return self::errorExit(9008, "There is a payment processor configuration problem. This is usually due to invalid account information or ip restrictions on the account. You can verify ip restriction by logging // into Manager. See Service Settings >> Allowed IP Addresses. "); case 12: // Hard decline from bank. return self::errorExit(9009, "Your transaction was declined "); case 13: // Voice authorization required. return self::errorExit(9010, "Your Transaction is pending. Contact Customer Service to complete your order."); case 23: // Issue with credit card number or expiration date. return self::errorExit(9011, "Invalid credit card information. Please re-enter."); case 26: return self::errorExit(9012, "You have not configured your payment processor with the correct credentials. Make sure you have provided both the and the variables "); default: return self::errorExit(9013, "Error - from payment processor: [" . $result_code . " " . $nvpArray['RESPMSG'] . "] "); } return self::errorExit(9014, "Check the code - all transactions should have been headed off before they got here. Something slipped through the net"); } /** * Produces error message and returns from class * * @param null $errorCode * @param null $errorMessage * * @return object */ public function &errorExit($errorCode = NULL, $errorMessage = NULL) { $e = CRM_Core_Error::singleton(); if ($errorCode) { $e->push($errorCode, 0, NULL, $errorMessage); } else { $e->push(9000, 0, NULL, 'Unknown System Error.'); } return $e; } /** * NOTE: 'doTransferCheckout' not implemented * * @param array $params * @param $component * * @throws Exception */ public function doTransferCheckout(&$params, $component) { CRM_Core_Error::fatal(ts('This function is not implemented')); } /** * This public function checks to see if we have the right processor config values set * * NOTE: Called by Events and Contribute to check config params are set prior to trying * register any credit card details * * @return string|null * the error message if any, null if OK */ public function checkConfig() { $errorMsg = array(); if (empty($this->_paymentProcessor['user_name'])) { $errorMsg[] = ' ' . ts('ssl_merchant_id is not set for this payment processor'); } if (empty($this->_paymentProcessor['url_site'])) { $errorMsg[] = ' ' . ts('URL is not set for %1', array(1 => $this->_paymentProcessor['name'])); } if (!empty($errorMsg)) { return implode('

', $errorMsg); } else { return NULL; } } /** * convert to a name/value pair (nvp) string * * @param $payflow_query_array * * @return array|string */ public function convert_to_nvp($payflow_query_array) { foreach ($payflow_query_array as $key => $value) { $payflow_query[] = $key . '[' . strlen($value) . ']=' . $value; } $payflow_query = implode('&', $payflow_query); return $payflow_query; } /** * Submit transaction using cURL * * @param string $submiturl Url to direct HTTPS GET to * @param $payflow_query value string to be posted * * @return mixed|object */ public function submit_transaction($submiturl, $payflow_query) { // get data ready for API $user_agent = $_SERVER['HTTP_USER_AGENT']; // Here's your custom headers; adjust appropriately for your setup: $headers[] = "Content-Type: text/namevalue"; //or text/xml if using XMLPay. $headers[] = "Content-Length : " . strlen($data); // Length of data to be passed // Here the server timeout value is set to 45, but notice // below in the cURL section, the timeout // for cURL is 90 seconds. You want to make sure the server // timeout is less, then the connection. $headers[] = "X-VPS-Timeout: 45"; //random unique number - the transaction is retried using this transaction ID // in this function but if that doesn't work and it is re- submitted // it is treated as a new attempt. Payflow Pro doesn't allow // you to change details (e.g. card no) when you re-submit // you can only try the same details $headers[] = "X-VPS-Request-ID: " . rand(1, 1000000000); // optional header field $headers[] = "X-VPS-VIT-Integration-Product: CiviCRM"; // other Optional Headers. If used adjust as necessary. // Name of your OS //$headers[] = "X-VPS-VIT-OS-Name: Linux"; // OS Version //$headers[] = "X-VPS-VIT-OS-Version: RHEL 4"; // What you are using //$headers[] = "X-VPS-VIT-Client-Type: PHP/cURL"; // For your info //$headers[] = "X-VPS-VIT-Client-Version: 0.01"; // For your info //$headers[] = "X-VPS-VIT-Client-Architecture: x86"; // Application version //$headers[] = "X-VPS-VIT-Integration-Version: 0.01"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $submiturl); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_USERAGENT, $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 if (ini_get('open_basedir') == '' && ini_get('safe_mode') == 'Off') { curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0); } curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, Civi::settings()->get('verifySSL')); // this line makes it work under https curl_setopt($ch, CURLOPT_POSTFIELDS, $payflow_query); //adding POST data curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, Civi::settings()->get('verifySSL') ? 2 : 0); //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 // Try to submit the transaction up to 3 times with 5 second delay. This can be used // in case of network issues. The idea here is since you are posting via HTTPS there // could be general network issues, so try a few times before you tell customer there // is an issue. $i = 1; while ($i++ <= 3) { $responseData = curl_exec($ch); $responseHeaders = curl_getinfo($ch); if ($responseHeaders['http_code'] != 200) { // Let's wait 5 seconds to see if its a temporary network issue. sleep(5); } elseif ($responseHeaders['http_code'] == 200) { // we got a good response, drop out of loop. break; } } if ($responseHeaders['http_code'] != 200) { return self::errorExit(9015, "Error connecting to the Payflow Pro API server."); } /* * Transaction submitted - * See if we had a curl error - if so tell 'em and bail out * * NOTE: curl_error does not return a logical value (see its documentation), but * a string, which is empty when there was no error. */ if ((curl_errno($ch) > 0) || (strlen(curl_error($ch)) > 0)) { curl_close($ch); $errorNum = curl_errno($ch); $errorDesc = curl_error($ch); //Paranoia - in the unlikley event that 'curl' errno fails if ($errorNum == 0) { $errorNum = 9005; } // Paranoia - in the unlikley event that 'curl' error fails if (strlen($errorDesc) == 0) { $errorDesc = "Connection to payment gateway failed"; } if ($errorNum = 60) { return self::errorExit($errorNum, "Curl error - " . $errorDesc . " Try this link for more information http://curl.haxx.se/d ocs/sslcerts.html" ); } return self::errorExit($errorNum, "Curl error - " . $errorDesc . " processor response = " . $processorResponse ); } /* * If null data returned - tell 'em and bail out * * NOTE: You will not necessarily get a string back, if the request failed for * any reason, the return value will be the boolean false. */ if (($responseData === FALSE) || (strlen($responseData) == 0)) { curl_close($ch); return self::errorExit(9006, "Error: Connection to payment gateway failed - no data returned. Gateway url set to $submiturl"); } /* * If gateway returned no data - tell 'em and bail out */ if (empty($responseData)) { curl_close($ch); return self::errorExit(9007, "Error: No data returned from payment gateway."); } /* * Success so far - close the curl and check the data */ curl_close($ch); return $responseData; } }