Merge pull request #18019 from eileenmcnaughton/event_clean
[civicrm-core.git] / CRM / Core / Payment / PayflowPro.php
1 <?php
2 /*
3 +----------------------------------------------------------------------------+
4 | Payflow Pro Core Payment Module for CiviCRM version 5 |
5 +----------------------------------------------------------------------------+
6 | Licensed to CiviCRM under the Academic Free License version 3.0 |
7 | |
8 | Written & Contributed by Eileen McNaughton - 2009 |
9 +---------------------------------------------------------------------------+
10 */
11
12 use Civi\Payment\Exception\PaymentProcessorException;
13
14 /**
15 * Class CRM_Core_Payment_PayflowPro.
16 */
17 class CRM_Core_Payment_PayflowPro extends CRM_Core_Payment {
18
19 /**
20 * Constructor
21 *
22 * @param string $mode
23 * The mode of operation: live or test.
24 * @param $paymentProcessor
25 */
26 public function __construct($mode, &$paymentProcessor) {
27 // live or test
28 $this->_mode = $mode;
29 $this->_paymentProcessor = $paymentProcessor;
30 }
31
32 /*
33 * This function sends request and receives response from
34 * the processor. It is the main function for processing on-server
35 * credit card transactions
36 */
37
38 /**
39 * This function collects all the information from a web/api form and invokes
40 * the relevant payment processor specific functions to perform the transaction
41 *
42 * @param array $params
43 * Assoc array of input parameters for this transaction.
44 *
45 * @return array
46 * the result in an nice formatted array (or an error object)
47 * @abstract
48 */
49 public function doDirectPayment(&$params) {
50 if (!defined('CURLOPT_SSLCERT')) {
51 throw new PaymentProcessorException(ts('Payflow Pro requires curl with SSL support'));
52 }
53
54 /*
55 * define variables for connecting with the gateway
56 */
57
58 // Are you using the Payflow Fraud Protection Service?
59 // Default is YES, change to NO or blank if not.
60 //This has not been investigated as part of writing this payment processor
61 $fraud = 'NO';
62 //if you have not set up a separate user account the vendor name is used as the username
63 if (!$this->_paymentProcessor['subject']) {
64 $user = $this->_paymentProcessor['user_name'];
65 }
66 else {
67 $user = $this->_paymentProcessor['subject'];
68 }
69
70 // ideally this id would be passed through into this class as
71 // part of the paymentProcessor
72 //object with the other variables. It seems inefficient to re-query to get it.
73 //$params['processor_id'] = CRM_Core_DAO::getFieldValue(
74 // 'CRM_Contribute_DAO_ContributionP
75 //age',$params['contributionPageID'], 'payment_processor_id' );
76
77 /*
78 *Create the array of variables to be sent to the processor from the $params array
79 * passed into this function
80 *
81 */
82
83 $payflow_query_array = [
84 'USER' => $user,
85 'VENDOR' => $this->_paymentProcessor['user_name'],
86 'PARTNER' => $this->_paymentProcessor['signature'],
87 'PWD' => $this->_paymentProcessor['password'],
88 // C - Direct Payment using credit card
89 'TENDER' => 'C',
90 // A - Authorization, S - Sale
91 'TRXTYPE' => 'S',
92 'ACCT' => urlencode($params['credit_card_number']),
93 'CVV2' => $params['cvv2'],
94 'EXPDATE' => urlencode(sprintf('%02d', (int) $params['month']) . substr($params['year'], 2, 2)),
95 'ACCTTYPE' => urlencode($params['credit_card_type']),
96 'AMT' => urlencode($this->getAmount($params)),
97 'CURRENCY' => urlencode($params['currency']),
98 'FIRSTNAME' => $params['billing_first_name'],
99 //credit card name
100 'LASTNAME' => $params['billing_last_name'],
101 //credit card name
102 'STREET' => $params['street_address'],
103 'CITY' => urlencode($params['city']),
104 'STATE' => urlencode($params['state_province']),
105 'ZIP' => urlencode($params['postal_code']),
106 'COUNTRY' => urlencode($params['country']),
107 'EMAIL' => $params['email'],
108 'CUSTIP' => urlencode($params['ip_address']),
109 'COMMENT1' => urlencode($params['contributionType_accounting_code']),
110 'COMMENT2' => $mode,
111 'INVNUM' => urlencode($params['invoiceID']),
112 'ORDERDESC' => urlencode($params['description']),
113 'VERBOSITY' => 'MEDIUM',
114 'BILLTOCOUNTRY' => urlencode($params['country']),
115 ];
116
117 if ($params['installments'] == 1) {
118 $params['is_recur'] = FALSE;
119 }
120
121 if ($params['is_recur'] == TRUE) {
122
123 $payflow_query_array['TRXTYPE'] = 'R';
124 $payflow_query_array['OPTIONALTRX'] = 'S';
125 $payflow_query_array['OPTIONALTRXAMT'] = $this->getAmount($params);
126 //Amount of the initial Transaction. Required
127 $payflow_query_array['ACTION'] = 'A';
128 //A for add recurring (M-modify,C-cancel,R-reactivate,I-inquiry,P-payment
129 $payflow_query_array['PROFILENAME'] = urlencode('RegularContribution');
130 //A for add recurring (M-modify,C-cancel,R-reactivate,I-inquiry,P-payment
131 if ($params['installments'] > 0) {
132 $payflow_query_array['TERM'] = $params['installments'] - 1;
133 //ie. in addition to the one happening with this transaction
134 }
135 // $payflow_query_array['COMPANYNAME']
136 // $payflow_query_array['DESC'] = not set yet Optional
137 // description of the goods or
138 //services being purchased.
139 //This parameter applies only for ACH_CCD accounts.
140 // The
141 // $payflow_query_array['MAXFAILPAYMENTS'] = 0;
142 // number of payment periods (as s
143 //pecified by PAYPERIOD) for which the transaction is allowed
144 //to fail before PayPal cancels a profile. the default
145 // value of 0 (zero) specifies no
146 //limit. Retry
147 //attempts occur until the term is complete.
148 // $payflow_query_array['RETRYNUMDAYS'] = (not set as can't assume business rule
149
150 switch ($params['frequency_unit']) {
151 case '1 week':
152 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m"), date("d") + 7,
153 date("Y")
154 );
155 $params['end_date'] = mktime(0, 0, 0, date("m"), date("d") + (7 * $payflow_query_array['TERM']),
156 date("Y")
157 );
158 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']);
159 $payflow_query_array['PAYPERIOD'] = "WEEK";
160 $params['frequency_unit'] = "week";
161 $params['frequency_interval'] = 1;
162 break;
163
164 case '2 weeks':
165 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m"), date("d") + 14, date("Y"));
166 $params['end_date'] = mktime(0, 0, 0, date("m"), date("d") + (14 * $payflow_query_array['TERM']), date("Y ")
167 );
168 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']);
169 $payflow_query_array['PAYPERIOD'] = "BIWK";
170 $params['frequency_unit'] = "week";
171 $params['frequency_interval'] = 2;
172 break;
173
174 case '4 weeks':
175 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m"), date("d") + 28, date("Y")
176 );
177 $params['end_date'] = mktime(0, 0, 0, date("m"), date("d") + (28 * $payflow_query_array['TERM']), date("Y")
178 );
179 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']);
180 $payflow_query_array['PAYPERIOD'] = "FRWK";
181 $params['frequency_unit'] = "week";
182 $params['frequency_interval'] = 4;
183 break;
184
185 case '1 month':
186 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m") + 1,
187 date("d"), date("Y")
188 );
189 $params['end_date'] = mktime(0, 0, 0, date("m") +
190 (1 * $payflow_query_array['TERM']),
191 date("d"), date("Y")
192 );
193 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']);
194 $payflow_query_array['PAYPERIOD'] = "MONT";
195 $params['frequency_unit'] = "month";
196 $params['frequency_interval'] = 1;
197 break;
198
199 case '3 months':
200 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m") + 3, date("d"), date("Y")
201 );
202 $params['end_date'] = mktime(0, 0, 0, date("m") +
203 (3 * $payflow_query_array['TERM']),
204 date("d"), date("Y")
205 );
206 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']);
207 $payflow_query_array['PAYPERIOD'] = "QTER";
208 $params['frequency_unit'] = "month";
209 $params['frequency_interval'] = 3;
210 break;
211
212 case '6 months':
213 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m") + 6, date("d"),
214 date("Y")
215 );
216 $params['end_date'] = mktime(0, 0, 0, date("m") +
217 (6 * $payflow_query_array['TERM']),
218 date("d"), date("Y")
219 );
220 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']
221 );
222 $payflow_query_array['PAYPERIOD'] = "SMYR";
223 $params['frequency_unit'] = "month";
224 $params['frequency_interval'] = 6;
225 break;
226
227 case '1 year':
228 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m"), date("d"),
229 date("Y") + 1
230 );
231 $params['end_date'] = mktime(0, 0, 0, date("m"), date("d"),
232 date("Y") +
233 (1 * $payflow_query_array['TEM'])
234 );
235 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']);
236 $payflow_query_array['PAYPERIOD'] = "YEAR";
237 $params['frequency_unit'] = "year";
238 $params['frequency_interval'] = 1;
239 break;
240 }
241 }
242
243 CRM_Utils_Hook::alterPaymentProcessorParams($this, $params, $payflow_query_array);
244 $payflow_query = $this->convert_to_nvp($payflow_query_array);
245
246 /*
247 * Check to see if we have a duplicate before we send
248 */
249 if ($this->checkDupe($params['invoiceID'], CRM_Utils_Array::value('contributionID', $params))) {
250 throw new PaymentProcessorException('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.', 9003);
251 }
252
253 // ie. url at payment processor to submit to.
254 $submiturl = $this->_paymentProcessor['url_site'];
255
256 $responseData = self::submit_transaction($submiturl, $payflow_query);
257
258 /*
259 * Payment successfully sent to gateway - process the response now
260 */
261 $result = strstr($responseData, 'RESULT');
262 if (empty($result)) {
263 throw new PaymentProcessorException('No RESULT code from PayPal.', 9016);
264 }
265
266 $nvpArray = [];
267 while (strlen($result)) {
268 // name
269 $keypos = strpos($result, '=');
270 $keyval = substr($result, 0, $keypos);
271 // value
272 $valuepos = strpos($result, '&') ? strpos($result, '&') : strlen($result);
273 $valval = substr($result, $keypos + 1, $valuepos - $keypos - 1);
274 // decoding the respose
275 $nvpArray[$keyval] = $valval;
276 $result = substr($result, $valuepos + 1, strlen($result));
277 }
278 // get the result code to validate.
279 $result_code = $nvpArray['RESULT'];
280 /*debug
281 echo "<p>Params array</p><br>";
282 print_r($params);
283 echo "<p></p><br>";
284 echo "<p>Values to Payment Processor</p><br>";
285 print_r($payflow_query_array);
286 echo "<p></p><br>";
287 echo "<p>Results from Payment Processor</p><br>";
288 print_r($nvpArray);
289 echo "<p></p><br>";
290 */
291
292 switch ($result_code) {
293 case 0:
294
295 /*******************************************************
296 * Success !
297 * This is a successful transaction. Payflow Pro does return further information
298 * about transactions to help you identify fraud including whether they pass
299 * the cvv check, the avs check. This is stored in
300 * CiviCRM as part of the transact
301 * but not further processing is done. Business rules would need to be defined
302 *******************************************************/
303 $params['trxn_id'] = $nvpArray['PNREF'] . $nvpArray['TRXPNREF'];
304 //'trxn_id' is varchar(255) field. returned value is length 12
305 $params['trxn_result_code'] = $nvpArray['AUTHCODE'] . "-Cvv2:" . $nvpArray['CVV2MATCH'] . "-avs:" . $nvpArray['AVSADDR'];
306
307 if ($params['is_recur'] == TRUE) {
308 $params['recur_trxn_id'] = $nvpArray['PROFILEID'];
309 //'trxn_id' is varchar(255) field. returned value is length 12
310 }
311 return $params;
312
313 case 1:
314 throw new PaymentProcessorException('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. ', 9003);
315
316 case 12:
317 // Hard decline from bank.
318 throw new PaymentProcessorException('Your transaction was declined ', 9009);
319
320 case 13:
321 // Voice authorization required.
322 throw new PaymentProcessorException('Your Transaction is pending. Contact Customer Service to complete your order.', 9010);
323
324 case 23:
325 // Issue with credit card number or expiration date.
326 throw new PaymentProcessorException('Invalid credit card information. Please re-enter.', 9011);
327
328 case 26:
329 throw new PaymentProcessorException('You have not configured your payment processor with the correct credentials. Make sure you have provided both the <vendor> and the <user> variables ', 9012);
330
331 default:
332 throw new PaymentProcessorException('Error - from payment processor: [' . $result_code . " " . $nvpArray['RESPMSG'] . "] ", 9013);
333 }
334 }
335
336 /**
337 * NOTE: 'doTransferCheckout' not implemented
338 *
339 * @param array $params
340 * @param $component
341 *
342 * @throws Exception
343 */
344 public function doTransferCheckout(&$params, $component) {
345 throw new CRM_Core_Exception(ts('This function is not implemented'));
346 }
347
348 /**
349 * This public function checks to see if we have the right processor config values set
350 *
351 * NOTE: Called by Events and Contribute to check config params are set prior to trying
352 * register any credit card details
353 *
354 * @return string|null
355 * the error message if any, null if OK
356 */
357 public function checkConfig() {
358 $errorMsg = [];
359 if (empty($this->_paymentProcessor['user_name'])) {
360 $errorMsg[] = ' ' . ts('ssl_merchant_id is not set for this payment processor');
361 }
362
363 if (empty($this->_paymentProcessor['url_site'])) {
364 $errorMsg[] = ' ' . ts('URL is not set for %1', [1 => $this->_paymentProcessor['name']]);
365 }
366
367 if (!empty($errorMsg)) {
368 return implode('<p>', $errorMsg);
369 }
370 else {
371 return NULL;
372 }
373 }
374
375 /**
376 * convert to a name/value pair (nvp) string
377 *
378 * @param $payflow_query_array
379 *
380 * @return array|string
381 */
382 public function convert_to_nvp($payflow_query_array) {
383 foreach ($payflow_query_array as $key => $value) {
384 $payflow_query[] = $key . '[' . strlen($value) . ']=' . $value;
385 }
386 $payflow_query = implode('&', $payflow_query);
387
388 return $payflow_query;
389 }
390
391 /**
392 * Submit transaction using cURL
393 *
394 * @param string $submiturl Url to direct HTTPS GET to
395 * @param $payflow_query value string to be posted
396 *
397 * @return mixed|object
398 */
399 public function submit_transaction($submiturl, $payflow_query) {
400 // get data ready for API
401 $user_agent = $_SERVER['HTTP_USER_AGENT'];
402 // Here's your custom headers; adjust appropriately for your setup:
403 $headers[] = "Content-Type: text/namevalue";
404 //or text/xml if using XMLPay.
405 $headers[] = "Content-Length : " . strlen($data);
406 // Length of data to be passed
407 // Here the server timeout value is set to 45, but notice
408 // below in the cURL section, the timeout
409 // for cURL is 90 seconds. You want to make sure the server
410 // timeout is less, then the connection.
411 $headers[] = "X-VPS-Timeout: 45";
412 //random unique number - the transaction is retried using this transaction ID
413 // in this function but if that doesn't work and it is re- submitted
414 // it is treated as a new attempt. Payflow Pro doesn't allow
415 // you to change details (e.g. card no) when you re-submit
416 // you can only try the same details
417 $headers[] = "X-VPS-Request-ID: " . rand(1, 1000000000);
418 // optional header field
419 $headers[] = "X-VPS-VIT-Integration-Product: CiviCRM";
420 // other Optional Headers. If used adjust as necessary.
421 // Name of your OS
422 //$headers[] = "X-VPS-VIT-OS-Name: Linux";
423 // OS Version
424 //$headers[] = "X-VPS-VIT-OS-Version: RHEL 4";
425 // What you are using
426 //$headers[] = "X-VPS-VIT-Client-Type: PHP/cURL";
427 // For your info
428 //$headers[] = "X-VPS-VIT-Client-Version: 0.01";
429 // For your info
430 //$headers[] = "X-VPS-VIT-Client-Architecture: x86";
431 // Application version
432 //$headers[] = "X-VPS-VIT-Integration-Version: 0.01";
433 $ch = curl_init();
434 curl_setopt($ch, CURLOPT_URL, $submiturl);
435 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
436 curl_setopt($ch, CURLOPT_USERAGENT, $user_agent);
437 curl_setopt($ch, CURLOPT_HEADER, 1);
438 // tells curl to include headers in response
439 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
440 // return into a variable
441 curl_setopt($ch, CURLOPT_TIMEOUT, 90);
442 // times out after 90 secs
443 if (ini_get('open_basedir') == '' && ini_get('safe_mode') == 'Off') {
444 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
445 }
446 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, Civi::settings()->get('verifySSL'));
447 // this line makes it work under https
448 curl_setopt($ch, CURLOPT_POSTFIELDS, $payflow_query);
449 //adding POST data
450 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, Civi::settings()->get('verifySSL') ? 2 : 0);
451 //verifies ssl certificate
452 curl_setopt($ch, CURLOPT_FORBID_REUSE, TRUE);
453 //forces closure of connection when done
454 curl_setopt($ch, CURLOPT_POST, 1);
455 //data sent as POST
456
457 // Try to submit the transaction up to 3 times with 5 second delay. This can be used
458 // in case of network issues. The idea here is since you are posting via HTTPS there
459 // could be general network issues, so try a few times before you tell customer there
460 // is an issue.
461
462 $i = 1;
463 while ($i++ <= 3) {
464 $responseData = curl_exec($ch);
465 $responseHeaders = curl_getinfo($ch);
466 if ($responseHeaders['http_code'] != 200) {
467 // Let's wait 5 seconds to see if its a temporary network issue.
468 sleep(5);
469 }
470 elseif ($responseHeaders['http_code'] == 200) {
471 // we got a good response, drop out of loop.
472 break;
473 }
474 }
475 if ($responseHeaders['http_code'] != 200) {
476 throw new PaymentProcessorException('Error connecting to the Payflow Pro API server.', 9015);
477 }
478
479 /*
480 * Transaction submitted -
481 * See if we had a curl error - if so tell 'em and bail out
482 *
483 * NOTE: curl_error does not return a logical value (see its documentation), but
484 * a string, which is empty when there was no error.
485 */
486 if ((curl_errno($ch) > 0) || (strlen(curl_error($ch)) > 0)) {
487 curl_close($ch);
488 $errorNum = curl_errno($ch);
489 $errorDesc = curl_error($ch);
490
491 //Paranoia - in the unlikley event that 'curl' errno fails
492 if ($errorNum == 0) {
493 $errorNum = 9005;
494 }
495
496 // Paranoia - in the unlikley event that 'curl' error fails
497 if (strlen($errorDesc) == 0) {
498 $errorDesc = "Connection to payment gateway failed";
499 }
500 if ($errorNum = 60) {
501 throw new PaymentProcessorException('Curl error - ' . $errorDesc .
502 ' Try this link for more information http://curl.haxx.se/d
503 ocs/sslcerts.html', $errorNum);
504 }
505
506 throw new PaymentProcessorException("Curl error - " . $errorDesc .
507 " processor response = " . $processorResponse, $errorNum
508 );
509 }
510
511 /*
512 * If null data returned - tell 'em and bail out
513 *
514 * NOTE: You will not necessarily get a string back, if the request failed for
515 * any reason, the return value will be the boolean false.
516 */
517 if (($responseData === FALSE) || (strlen($responseData) == 0)) {
518 curl_close($ch);
519 throw new PaymentProcessorException("Error: Connection to payment gateway failed - no data
520 returned. Gateway url set to $submiturl", 9006);
521 }
522
523 /*
524 * If gateway returned no data - tell 'em and bail out
525 */
526 if (empty($responseData)) {
527 curl_close($ch);
528 throw new PaymentProcessorException('Error: No data returned from payment gateway.', 9007);
529 }
530
531 /*
532 * Success so far - close the curl and check the data
533 */
534 curl_close($ch);
535 return $responseData;
536 }
537
538 }