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