3 +----------------------------------------------------------------------------+
4 | PayflowPro Core Payment Module for CiviCRM version 4.7 |
5 +----------------------------------------------------------------------------+
6 | Licensed to CiviCRM under the Academic Free License version 3.0 |
8 | Written & Contributed by Eileen McNaughton - 2009 |
9 +---------------------------------------------------------------------------+
13 * Class CRM_Core_Payment_PayflowPro.
15 class CRM_Core_Payment_PayflowPro
extends CRM_Core_Payment
{
16 // (not used, implicit in the API, might need to convert?)
21 * We only need one instance of this object. So we use the singleton
22 * pattern and cache the instance in this variable
26 static private $_singleton = NULL;
32 * The mode of operation: live or test.
33 * @param $paymentProcessor
35 public function __construct($mode, &$paymentProcessor) {
38 $this->_paymentProcessor
= $paymentProcessor;
39 $this->_processorName
= ts('Payflow Pro');
43 * This function sends request and receives response from
44 * the processor. It is the main function for processing on-server
45 * credit card transactions
48 * This function collects all the information from a web/api form and invokes
49 * the relevant payment processor specific functions to perform the transaction
51 * @param array $params
52 * Assoc array of input parameters for this transaction.
55 * the result in an nice formatted array (or an error object)
58 public function doDirectPayment(&$params) {
59 if (!defined('CURLOPT_SSLCERT')) {
60 CRM_Core_Error
::fatal(ts('PayFlowPro requires curl with SSL support'));
64 * define variables for connecting with the gateway
67 // Are you using the Payflow Fraud Protection Service?
68 // Default is YES, change to NO or blank if not.
69 //This has not been investigated as part of writing this payment processor
71 //if you have not set up a separate user account the vendor name is used as the username
72 if (!$this->_paymentProcessor
['subject']) {
73 $user = $this->_paymentProcessor
['user_name'];
76 $user = $this->_paymentProcessor
['subject'];
79 // ideally this id would be passed through into this class as
80 // part of the paymentProcessor
81 //object with the other variables. It seems inefficient to re-query to get it.
82 //$params['processor_id'] = CRM_Core_DAO::getFieldValue(
83 // 'CRM_Contribute_DAO_ContributionP
84 //age',$params['contributionPageID'], 'payment_processor_id' );
87 *Create the array of variables to be sent to the processor from the $params array
88 * passed into this function
92 $payflow_query_array = array(
94 'VENDOR' => $this->_paymentProcessor
['user_name'],
95 'PARTNER' => $this->_paymentProcessor
['signature'],
96 'PWD' => $this->_paymentProcessor
['password'],
97 // C - Direct Payment using credit card
99 // A - Authorization, S - Sale
101 'ACCT' => urlencode($params['credit_card_number']),
102 'CVV2' => $params['cvv2'],
103 'EXPDATE' => urlencode(sprintf('%02d', (int) $params['month']) . substr($params['year'], 2, 2)),
104 'ACCTTYPE' => urlencode($params['credit_card_type']),
105 'AMT' => urlencode($params['amount']),
106 'CURRENCY' => urlencode($params['currency']),
107 'FIRSTNAME' => $params['billing_first_name'],
109 'LASTNAME' => $params['billing_last_name'],
111 'STREET' => $params['street_address'],
112 'CITY' => urlencode($params['city']),
113 'STATE' => urlencode($params['state_province']),
114 'ZIP' => urlencode($params['postal_code']),
115 'COUNTRY' => urlencode($params['country']),
116 'EMAIL' => $params['email'],
117 'CUSTIP' => urlencode($params['ip_address']),
118 'COMMENT1' => urlencode($params['contributionType_accounting_code']),
120 'INVNUM' => urlencode($params['invoiceID']),
121 'ORDERDESC' => urlencode($params['description']),
122 'VERBOSITY' => 'MEDIUM',
123 'BILLTOCOUNTRY' => urlencode($params['country']),
126 if ($params['installments'] == 1) {
127 $params['is_recur'] = FALSE;
130 if ($params['is_recur'] == TRUE) {
132 $payflow_query_array['TRXTYPE'] = 'R';
133 $payflow_query_array['OPTIONALTRX'] = 'S';
134 $payflow_query_array['OPTIONALTRXAMT'] = $params['amount'];
135 //Amount of the initial Transaction. Required
136 $payflow_query_array['ACTION'] = 'A';
137 //A for add recurring (M-modify,C-cancel,R-reactivate,I-inquiry,P-payment
138 $payflow_query_array['PROFILENAME'] = urlencode('RegularContribution');
139 //A for add recurring (M-modify,C-cancel,R-reactivate,I-inquiry,P-payment
140 if ($params['installments'] > 0) {
141 $payflow_query_array['TERM'] = $params['installments'] - 1;
142 //ie. in addition to the one happening with this transaction
144 // $payflow_query_array['COMPANYNAME']
145 // $payflow_query_array['DESC'] = not set yet Optional
146 // description of the goods or
147 //services being purchased.
148 //This parameter applies only for ACH_CCD accounts.
150 // $payflow_query_array['MAXFAILPAYMENTS'] = 0;
151 // number of payment periods (as s
152 //pecified by PAYPERIOD) for which the transaction is allowed
153 //to fail before PayPal cancels a profile. the default
154 // value of 0 (zero) specifies no
156 //attempts occur until the term is complete.
157 // $payflow_query_array['RETRYNUMDAYS'] = (not set as can't assume business rule
159 switch ($params['frequency_unit']) {
161 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m"), date("d") +
7,
164 $params['end_date'] = mktime(0, 0, 0, date("m"), date("d") +
(7 * $payflow_query_array['TERM']),
167 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']);
168 $payflow_query_array['PAYPERIOD'] = "WEEK";
169 $params['frequency_unit'] = "week";
170 $params['frequency_interval'] = 1;
174 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m"), date("d") +
14, date("Y"));
175 $params['end_date'] = mktime(0, 0, 0, date("m"), date("d") +
(14 * $payflow_query_array['TERM']), date("Y ")
177 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']);
178 $payflow_query_array['PAYPERIOD'] = "BIWK";
179 $params['frequency_unit'] = "week";
180 $params['frequency_interval'] = 2;
184 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m"), date("d") +
28, date("Y")
186 $params['end_date'] = mktime(0, 0, 0, date("m"), date("d") +
(28 * $payflow_query_array['TERM']), date("Y")
188 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']);
189 $payflow_query_array['PAYPERIOD'] = "FRWK";
190 $params['frequency_unit'] = "week";
191 $params['frequency_interval'] = 4;
195 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m") +
1,
198 $params['end_date'] = mktime(0, 0, 0, date("m") +
199 (1 * $payflow_query_array['TERM']),
202 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']);
203 $payflow_query_array['PAYPERIOD'] = "MONT";
204 $params['frequency_unit'] = "month";
205 $params['frequency_interval'] = 1;
209 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m") +
3, date("d"), date("Y")
211 $params['end_date'] = mktime(0, 0, 0, date("m") +
212 (3 * $payflow_query_array['TERM']),
215 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']);
216 $payflow_query_array['PAYPERIOD'] = "QTER";
217 $params['frequency_unit'] = "month";
218 $params['frequency_interval'] = 3;
222 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m") +
6, date("d"),
225 $params['end_date'] = mktime(0, 0, 0, date("m") +
226 (6 * $payflow_query_array['TERM']),
229 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']
231 $payflow_query_array['PAYPERIOD'] = "SMYR";
232 $params['frequency_unit'] = "month";
233 $params['frequency_interval'] = 6;
237 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m"), date("d"),
240 $params['end_date'] = mktime(0, 0, 0, date("m"), date("d"),
242 (1 * $payflow_query_array['TEM'])
244 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']);
245 $payflow_query_array['PAYPERIOD'] = "YEAR";
246 $params['frequency_unit'] = "year";
247 $params['frequency_interval'] = 1;
252 CRM_Utils_Hook
::alterPaymentProcessorParams($this, $params, $payflow_query_array);
253 $payflow_query = $this->convert_to_nvp($payflow_query_array);
256 * Check to see if we have a duplicate before we send
258 if ($this->checkDupe($params['invoiceID'], CRM_Utils_Array
::value('contributionID', $params))) {
259 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.');
262 // ie. url at payment processor to submit to.
263 $submiturl = $this->_paymentProcessor
['url_site'];
265 $responseData = self
::submit_transaction($submiturl, $payflow_query);
268 * Payment successfully sent to gateway - process the response now
270 $result = strstr($responseData, "RESULT");
271 if (empty($result)) {
272 return self
::errorExit(9016, "No RESULT code from PayPal.");
276 while (strlen($result)) {
278 $keypos = strpos($result, '=');
279 $keyval = substr($result, 0, $keypos);
281 $valuepos = strpos($result, '&') ?
strpos($result, '&') : strlen($result);
282 $valval = substr($result, $keypos +
1, $valuepos - $keypos - 1);
283 // decoding the respose
284 $nvpArray[$keyval] = $valval;
285 $result = substr($result, $valuepos +
1, strlen($result));
287 // get the result code to validate.
288 $result_code = $nvpArray['RESULT'];
290 echo "<p>Params array</p><br>";
293 echo "<p>Values to Payment Processor</p><br>";
294 print_r($payflow_query_array);
296 echo "<p>Results from Payment Processor</p><br>";
301 switch ($result_code) {
304 /*******************************************************
306 * This is a successful transaction. PayFlow Pro does return further information
307 * about transactions to help you identify fraud including whether they pass
308 * the cvv check, the avs check. This is stored in
309 * CiviCRM as part of the transact
310 * but not further processing is done. Business rules would need to be defined
311 *******************************************************/
312 $params['trxn_id'] = $nvpArray['PNREF'] . $nvpArray['TRXPNREF'];
313 //'trxn_id' is varchar(255) field. returned value is length 12
314 $params['trxn_result_code'] = $nvpArray['AUTHCODE'] . "-Cvv2:" . $nvpArray['CVV2MATCH'] . "-avs:" . $nvpArray['AVSADDR'];
316 if ($params['is_recur'] == TRUE) {
317 $params['recur_trxn_id'] = $nvpArray['PROFILEID'];
318 //'trxn_id' is varchar(255) field. returned value is length 12
323 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. ");
326 // Hard decline from bank.
327 return self
::errorExit(9009, "Your transaction was declined ");
330 // Voice authorization required.
331 return self
::errorExit(9010, "Your Transaction is pending. Contact Customer Service to complete your order.");
334 // Issue with credit card number or expiration date.
335 return self
::errorExit(9011, "Invalid credit card information. Please re-enter.");
338 return self
::errorExit(9012, "You have not configured your payment processor with the correct credentials. Make sure you have provided both the <vendor> and the <user> variables ");
341 return self
::errorExit(9013, "Error - from payment processor: [" . $result_code . " " . $nvpArray['RESPMSG'] . "] ");
344 return self
::errorExit(9014, "Check the code - all transactions should have been headed off before they got here. Something slipped through the net");
348 * Produces error message and returns from class
351 * @param null $errorCode
352 * @param null $errorMessage
356 public function &errorExit($errorCode = NULL, $errorMessage = NULL) {
357 $e = CRM_Core_Error
::singleton();
359 $e->push($errorCode, 0, NULL, $errorMessage);
362 $e->push(9000, 0, NULL, 'Unknown System Error.');
369 * NOTE: 'doTransferCheckout' not implemented
372 * @param array $params
377 public function doTransferCheckout(&$params, $component) {
378 CRM_Core_Error
::fatal(ts('This function is not implemented'));
382 * This public function checks to see if we have the right processor config values set
384 * NOTE: Called by Events and Contribute to check config params are set prior to trying
385 * register any credit card details
387 * @param string $mode
388 * The mode we are operating in (live or test) - not used.
390 * returns string $errorMsg if any errors found - null if OK
393 // function checkConfig( $mode ) // CiviCRM V1.9 Declaration
396 * CiviCRM V2.0 Declaration
397 * This function checks to see if we have the right config values
399 * @internal param string $mode the mode we are operating in (live or test)
402 * the error message if any
404 public function checkConfig() {
406 if (empty($this->_paymentProcessor
['user_name'])) {
407 $errorMsg[] = ' ' . ts('ssl_merchant_id is not set for this payment processor');
410 if (empty($this->_paymentProcessor
['url_site'])) {
411 $errorMsg[] = ' ' . ts('URL is not set for %1', array(1 => $this->_paymentProcessor
['name']));
414 if (!empty($errorMsg)) {
415 return implode('<p>', $errorMsg);
424 * convert to a name/value pair (nvp) string
427 * @param $payflow_query_array
429 * @return array|string
431 public function convert_to_nvp($payflow_query_array) {
432 foreach ($payflow_query_array as $key => $value) {
433 $payflow_query[] = $key . '[' . strlen($value) . ']=' . $value;
435 $payflow_query = implode('&', $payflow_query);
437 return $payflow_query;
441 * Submit transaction using CuRL
442 * @submiturl string Url to direct HTTPS GET to
443 * @payflow_query value string to be posted
447 * @param $payflow_query
449 * @return mixed|object
451 public function submit_transaction($submiturl, $payflow_query) {
453 * Submit transaction using CuRL
456 // get data ready for API
457 $user_agent = $_SERVER['HTTP_USER_AGENT'];
458 // Here's your custom headers; adjust appropriately for your setup:
459 $headers[] = "Content-Type: text/namevalue";
460 //or text/xml if using XMLPay.
461 $headers[] = "Content-Length : " . strlen($data);
462 // Length of data to be passed
463 // Here the server timeout value is set to 45, but notice
464 // below in the cURL section, the timeout
465 // for cURL is 90 seconds. You want to make sure the server
466 // timeout is less, then the connection.
467 $headers[] = "X-VPS-Timeout: 45";
468 //random unique number - the transaction is retried using this transaction ID
469 // in this function but if that doesn't work and it is re- submitted
470 // it is treated as a new attempt. PayflowPro doesn't allow
471 // you to change details (e.g. card no) when you re-submit
472 // you can only try the same details
473 $headers[] = "X-VPS-Request-ID: " . rand(1, 1000000000);
474 // optional header field
475 $headers[] = "X-VPS-VIT-Integration-Product: CiviCRM";
476 // other Optional Headers. If used adjust as necessary.
478 //$headers[] = "X-VPS-VIT-OS-Name: Linux";
480 //$headers[] = "X-VPS-VIT-OS-Version: RHEL 4";
481 // What you are using
482 //$headers[] = "X-VPS-VIT-Client-Type: PHP/cURL";
484 //$headers[] = "X-VPS-VIT-Client-Version: 0.01";
486 //$headers[] = "X-VPS-VIT-Client-Architecture: x86";
487 // Application version
488 //$headers[] = "X-VPS-VIT-Integration-Version: 0.01";
490 curl_setopt($ch, CURLOPT_URL
, $submiturl);
491 curl_setopt($ch, CURLOPT_HTTPHEADER
, $headers);
492 curl_setopt($ch, CURLOPT_USERAGENT
, $user_agent);
493 curl_setopt($ch, CURLOPT_HEADER
, 1);
494 // tells curl to include headers in response
495 curl_setopt($ch, CURLOPT_RETURNTRANSFER
, 1);
496 // return into a variable
497 curl_setopt($ch, CURLOPT_TIMEOUT
, 90);
498 // times out after 90 secs
499 if (ini_get('open_basedir') == '' && ini_get('safe_mode') == 'Off') {
500 curl_setopt($ch, CURLOPT_FOLLOWLOCATION
, 0);
502 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER
, Civi
::settings()->get('verifySSL'));
503 // this line makes it work under https
504 curl_setopt($ch, CURLOPT_POSTFIELDS
, $payflow_query);
506 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST
, Civi
::settings()->get('verifySSL') ?
2 : 0);
507 //verifies ssl certificate
508 curl_setopt($ch, CURLOPT_FORBID_REUSE
, TRUE);
509 //forces closure of connection when done
510 curl_setopt($ch, CURLOPT_POST
, 1);
513 // Try to submit the transaction up to 3 times with 5 second delay. This can be used
514 // in case of network issues. The idea here is since you are posting via HTTPS there
515 // could be general network issues, so try a few times before you tell customer there
520 $responseData = curl_exec($ch);
521 $responseHeaders = curl_getinfo($ch);
522 if ($responseHeaders['http_code'] != 200) {
523 // Let's wait 5 seconds to see if its a temporary network issue.
526 elseif ($responseHeaders['http_code'] == 200) {
527 // we got a good response, drop out of loop.
531 if ($responseHeaders['http_code'] != 200) {
532 return self
::errorExit(9015, "Error connecting to the payflo API server.");
536 * Transaction submitted -
537 * See if we had a curl error - if so tell 'em and bail out
539 * NOTE: curl_error does not return a logical value (see its documentation), but
540 * a string, which is empty when there was no error.
542 if ((curl_errno($ch) > 0) ||
(strlen(curl_error($ch)) > 0)) {
544 $errorNum = curl_errno($ch);
545 $errorDesc = curl_error($ch);
547 //Paranoia - in the unlikley event that 'curl' errno fails
548 if ($errorNum == 0) {
552 // Paranoia - in the unlikley event that 'curl' error fails
553 if (strlen($errorDesc) == 0) {
554 $errorDesc = "Connection to payment gateway failed";
556 if ($errorNum = 60) {
557 return self
::errorExit($errorNum, "Curl error - " . $errorDesc .
558 " Try this link for more information http://curl.haxx.se/d
563 return self
::errorExit($errorNum, "Curl error - " . $errorDesc .
564 " processor response = " . $processorResponse
569 * If null data returned - tell 'em and bail out
571 * NOTE: You will not necessarily get a string back, if the request failed for
572 * any reason, the return value will be the boolean false.
574 if (($responseData === FALSE) ||
(strlen($responseData) == 0)) {
576 return self
::errorExit(9006, "Error: Connection to payment gateway failed - no data
577 returned. Gateway url set to $submiturl");
581 * If gateway returned no data - tell 'em and bail out
583 if (empty($responseData)) {
585 return self
::errorExit(9007, "Error: No data returned from payment gateway.");
589 * Success so far - close the curl and check the data
592 return $responseData;
594 //end submit_transaction
597 * @param int $recurringProfileID
598 * @param int $processorID
602 public function getRecurringTransactionStatus($recurringProfileID, $processorID) {
603 if (!defined('CURLOPT_SSLCERT')) {
604 CRM_Core_Error
::fatal(ts('PayFlowPro requires curl with SSL support'));
608 * define variables for connecting with the gateway
611 //if you have not set up a separate user account the vendor name is used as the username
612 if (!$this->_paymentProcessor
['subject']) {
613 $user = $this->_paymentProcessor
['user_name'];
616 $user = $this->_paymentProcessor
['subject'];
618 //$recurringProfileID = "RT0000000001";
619 // c $trythis = $this->getRecurringTransactionStatus($recurringProfileID,17);
622 *Create the array of variables to be sent to the processor from the $params array
623 * passed into this function
627 $payflow_query_array = array(
629 'VENDOR' => $this->_paymentProcessor
['user_name'],
630 'PARTNER' => $this->_paymentProcessor
['signature'],
631 'PWD' => $this->_paymentProcessor
['password'],
632 // C - Direct Payment using credit card
634 // A - Authorization, S - Sale
637 //A for add recurring
638 //(M-modify,C-cancel,R-reactivate,
639 //I-inquiry,P-payment
640 'ORIGPROFILEID' => $recurringProfileID,
641 'PAYMENTHISTORY' => 'Y',
644 $payflow_query = $this->convert_to_nvp($payflow_query_array);
646 $submiturl = $this->_paymentProcessor
['url_site'];
647 //ie. url at payment processor to submit to.
648 $responseData = self
::submit_transaction($submiturl, $payflow_query);
650 * Payment successfully sent to gateway - process the response now
653 $result = strstr($responseData, "RESULT");
655 while (strlen($result)) {
657 $keypos = strpos($result, '=');
658 $keyval = substr($result, 0, $keypos);
660 $valuepos = strpos($result, '&') ?
strpos($result, '&') : strlen($result);
661 $valval = substr($result, $keypos +
1, $valuepos - $keypos - 1);
662 // decoding the respose
663 $nvpArray[$keyval] = $valval;
664 $result = substr($result, $valuepos +
1, strlen($result));
666 // get the result code to validate.
667 $result_code = $nvpArray['RESULT'];
668 print_r($responseData);
670 //RESPMSG=Invalid Profile ID: Invalid recurring profile ID