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