Merge pull request #18222 from civicrm/5.29
[civicrm-core.git] / CRM / Core / Payment / AuthorizeNet.php
1 <?php
2 /*
3 * Copyright (C) 2007
4 * Licensed to CiviCRM under the Academic Free License version 3.0.
5 *
6 * Written and contributed by Ideal Solution, LLC (http://www.idealso.com)
7 *
8 */
9
10 /**
11 *
12 * @package CRM
13 * @author Marshal Newrock <marshal@idealso.com>
14 */
15
16 use Civi\Payment\Exception\PaymentProcessorException;
17
18 /**
19 * NOTE:
20 * When looking up response codes in the Authorize.Net API, they
21 * begin at one, so always delete one from the "Position in Response"
22 */
23 class CRM_Core_Payment_AuthorizeNet extends CRM_Core_Payment {
24 const CHARSET = 'iso-8859-1';
25 const AUTH_APPROVED = 1;
26 const AUTH_DECLINED = 2;
27 const AUTH_ERROR = 3;
28 const AUTH_REVIEW = 4;
29 const TIMEZONE = 'America/Denver';
30
31 protected $_mode = NULL;
32
33 protected $_params = [];
34
35 /**
36 * @var GuzzleHttp\Client
37 */
38 protected $guzzleClient;
39
40 /**
41 * @return \GuzzleHttp\Client
42 */
43 public function getGuzzleClient(): \GuzzleHttp\Client {
44 return $this->guzzleClient ?? new \GuzzleHttp\Client();
45 }
46
47 /**
48 * @param \GuzzleHttp\Client $guzzleClient
49 */
50 public function setGuzzleClient(\GuzzleHttp\Client $guzzleClient) {
51 $this->guzzleClient = $guzzleClient;
52 }
53
54 /**
55 * Constructor.
56 *
57 * @param string $mode
58 * The mode of operation: live or test.
59 *
60 * @param $paymentProcessor
61 *
62 * @return \CRM_Core_Payment_AuthorizeNet
63 */
64 public function __construct($mode, &$paymentProcessor) {
65 $this->_mode = $mode;
66 $this->_paymentProcessor = $paymentProcessor;
67
68 $this->_setParam('apiLogin', $paymentProcessor['user_name']);
69 $this->_setParam('paymentKey', $paymentProcessor['password']);
70 $this->_setParam('paymentType', 'AIM');
71 $this->_setParam('md5Hash', $paymentProcessor['signature'] ?? NULL);
72
73 $this->_setParam('timestamp', time());
74 srand(time());
75 $this->_setParam('sequence', rand(1, 1000));
76 }
77
78 /**
79 * Should the first payment date be configurable when setting up back office recurring payments.
80 * In the case of Authorize.net this is an option
81 * @return bool
82 */
83 protected function supportsFutureRecurStartDate() {
84 return TRUE;
85 }
86
87 /**
88 * Can recurring contributions be set against pledges.
89 *
90 * In practice all processors that use the baseIPN function to finish transactions or
91 * call the completetransaction api support this by looking up previous contributions in the
92 * series and, if there is a prior contribution against a pledge, and the pledge is not complete,
93 * adding the new payment to the pledge.
94 *
95 * However, only enabling for processors it has been tested against.
96 *
97 * @return bool
98 */
99 protected function supportsRecurContributionsForPledges() {
100 return TRUE;
101 }
102
103 /**
104 * Submit a payment using Advanced Integration Method.
105 *
106 * @param array $params
107 * Assoc array of input parameters for this transaction.
108 *
109 * @return array
110 * the result in a nice formatted array (or an error object)
111 *
112 * @throws \Civi\Payment\Exception\PaymentProcessorException
113 */
114 public function doDirectPayment(&$params) {
115 if (!defined('CURLOPT_SSLCERT')) {
116 // Note that guzzle doesn't necessarily require CURL, although it prefers it. But we should leave this error
117 // here unless someone suggests it is not required since it's likely helpful.
118 throw new PaymentProcessorException('Authorize.Net requires curl with SSL support', 9001);
119 }
120
121 /*
122 * recurpayment function does not compile an array & then process it -
123 * - the tpl does the transformation so adding call to hook here
124 * & giving it a change to act on the params array
125 */
126 $newParams = $params;
127 if (!empty($params['is_recur']) && !empty($params['contributionRecurID'])) {
128 CRM_Utils_Hook::alterPaymentProcessorParams($this,
129 $params,
130 $newParams
131 );
132 }
133 foreach ($newParams as $field => $value) {
134 $this->_setParam($field, $value);
135 }
136
137 if (!empty($params['is_recur']) && !empty($params['contributionRecurID'])) {
138 $this->doRecurPayment();
139 return $params;
140 }
141
142 $postFields = [];
143 $authorizeNetFields = $this->_getAuthorizeNetFields();
144
145 // Set up our call for hook_civicrm_paymentProcessor,
146 // since we now have our parameters as assigned for the AIM back end.
147 CRM_Utils_Hook::alterPaymentProcessorParams($this,
148 $params,
149 $authorizeNetFields
150 );
151
152 foreach ($authorizeNetFields as $field => $value) {
153 // CRM-7419, since double quote is used as enclosure while doing csv parsing
154 $value = ($field == 'x_description') ? str_replace('"', "'", $value) : $value;
155 $postFields[] = $field . '=' . urlencode($value);
156 }
157
158 // Authorize.Net will not refuse duplicates, so we should check if the user already submitted this transaction
159 if ($this->checkDupe($authorizeNetFields['x_invoice_num'], CRM_Utils_Array::value('contributionID', $params))) {
160 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 from Authorize.net. 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.', 9004);
161 }
162
163 $response = (string) $this->getGuzzleClient()->post($this->_paymentProcessor['url_site'], [
164 'body' => implode('&', $postFields),
165 'curl' => [
166 CURLOPT_RETURNTRANSFER => TRUE,
167 CURLOPT_SSL_VERIFYPEER => Civi::settings()->get('verifySSL'),
168 ],
169 ])->getBody();
170
171 $response_fields = $this->explode_csv($response);
172
173 // fetch available contribution statuses
174 $contributionStatus = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
175
176 // check for application errors
177 // TODO:
178 // AVS, CVV2, CAVV, and other verification results
179 switch ($response_fields[0]) {
180 case self::AUTH_REVIEW:
181 $params['payment_status_id'] = array_search('Pending', $contributionStatus);
182 break;
183
184 case self::AUTH_ERROR:
185 $params['payment_status_id'] = array_search('Failed', $contributionStatus);
186 $errormsg = $response_fields[2] . ' ' . $response_fields[3];
187 throw new PaymentProcessorException($errormsg, $response_fields[1]);
188
189 case self::AUTH_DECLINED:
190 $errormsg = $response_fields[2] . ' ' . $response_fields[3];
191 throw new PaymentProcessorException($errormsg, $response_fields[1]);
192
193 default:
194 // Success
195 $params['trxn_id'] = !empty($response_fields[6]) ? $response_fields[6] : $this->getTestTrxnID();
196 break;
197 }
198
199 // TODO: include authorization code?
200
201 return $params;
202 }
203
204 /**
205 * Submit an Automated Recurring Billing subscription.
206 */
207 public function doRecurPayment() {
208 $template = CRM_Core_Smarty::singleton();
209
210 $intervalLength = $this->_getParam('frequency_interval');
211 $intervalUnit = $this->_getParam('frequency_unit');
212 if ($intervalUnit === 'week') {
213 $intervalLength *= 7;
214 $intervalUnit = 'days';
215 }
216 elseif ($intervalUnit === 'year') {
217 $intervalLength *= 12;
218 $intervalUnit = 'months';
219 }
220 elseif ($intervalUnit === 'day') {
221 $intervalUnit = 'days';
222 // interval cannot be less than 7 days or more than 1 year
223 if ($intervalLength < 7) {
224 throw new PaymentProcessorException('Payment interval must be at least one week', 9001);
225 }
226 if ($intervalLength > 365) {
227 throw new PaymentProcessorException('Payment interval may not be longer than one year', 9001);
228 }
229 }
230 elseif ($intervalUnit === 'month') {
231 $intervalUnit = 'months';
232 if ($intervalLength < 1) {
233 throw new PaymentProcessorException('Payment interval must be at least one week', 9001);
234 }
235 if ($intervalLength > 12) {
236 throw new PaymentProcessorException('Payment interval may not be longer than one year', 9001);
237 }
238 }
239
240 $template->assign('intervalLength', $intervalLength);
241 $template->assign('intervalUnit', $intervalUnit);
242
243 $template->assign('apiLogin', $this->_getParam('apiLogin'));
244 $template->assign('paymentKey', $this->_getParam('paymentKey'));
245 $template->assign('refId', substr($this->_getParam('invoiceID'), 0, 20));
246
247 //for recurring, carry first contribution id
248 $template->assign('invoiceNumber', $this->_getParam('contributionID'));
249 $firstPaymentDate = $this->_getParam('receive_date');
250 if (!empty($firstPaymentDate)) {
251 //allow for post dated payment if set in form
252 $startDate = date_create($firstPaymentDate);
253 }
254 else {
255 $startDate = date_create();
256 }
257 /* Format start date in Mountain Time to avoid Authorize.net error E00017
258 * we do this only if the day we are setting our start time to is LESS than the current
259 * day in mountaintime (ie. the server time of the A-net server). A.net won't accept a date
260 * earlier than the current date on it's server so if we are in PST we might need to use mountain
261 * time to bring our date forward. But if we are submitting something future dated we want
262 * the date we entered to be respected
263 */
264 $minDate = date_create('now', new DateTimeZone(self::TIMEZONE));
265 if (strtotime($startDate->format('Y-m-d')) < strtotime($minDate->format('Y-m-d'))) {
266 $startDate->setTimezone(new DateTimeZone(self::TIMEZONE));
267 }
268
269 $template->assign('startDate', $startDate->format('Y-m-d'));
270
271 $installments = $this->_getParam('installments');
272
273 // for open ended subscription totalOccurrences has to be 9999
274 $installments = empty($installments) ? 9999 : $installments;
275 $template->assign('totalOccurrences', $installments);
276
277 $template->assign('amount', $this->_getParam('amount'));
278
279 $template->assign('cardNumber', $this->_getParam('credit_card_number'));
280 $exp_month = str_pad($this->_getParam('month'), 2, '0', STR_PAD_LEFT);
281 $exp_year = $this->_getParam('year');
282 $template->assign('expirationDate', $exp_year . '-' . $exp_month);
283
284 // name rather than description is used in the tpl - see http://www.authorize.net/support/ARB_guide.pdf
285 $template->assign('name', $this->_getParam('description', TRUE));
286
287 $template->assign('email', $this->_getParam('email'));
288 $template->assign('contactID', $this->_getParam('contactID'));
289 $template->assign('billingFirstName', $this->_getParam('billing_first_name'));
290 $template->assign('billingLastName', $this->_getParam('billing_last_name'));
291 $template->assign('billingAddress', $this->_getParam('street_address', TRUE));
292 $template->assign('billingCity', $this->_getParam('city', TRUE));
293 $template->assign('billingState', $this->_getParam('state_province'));
294 $template->assign('billingZip', $this->_getParam('postal_code', TRUE));
295 $template->assign('billingCountry', $this->_getParam('country'));
296
297 $arbXML = $template->fetch('CRM/Contribute/Form/Contribution/AuthorizeNetARB.tpl');
298
299 // Submit to authorize.net
300 $response = $this->getGuzzleClient()->post($this->_paymentProcessor['url_recur'], [
301 'headers' => [
302 'Content-Type' => 'text/xml; charset=UTF8',
303 ],
304 'body' => $arbXML,
305 'curl' => [
306 CURLOPT_RETURNTRANSFER => TRUE,
307 CURLOPT_SSL_VERIFYPEER => Civi::settings()->get('verifySSL'),
308 ],
309 ]);
310 $responseFields = $this->_ParseArbReturn((string) $response->getBody());
311
312 if ($responseFields['resultCode'] === 'Error') {
313 throw new PaymentProcessorException($responseFields['text'], $responseFields['code']);
314 }
315
316 // update recur processor_id with subscriptionId
317 CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_ContributionRecur', $this->_getParam('contributionRecurID'),
318 'processor_id', $responseFields['subscriptionId']
319 );
320 //only impact of assigning this here is is can be used to cancel the subscription in an automated test
321 // if it isn't cancelled a duplicate transaction error occurs
322 if (!empty($responseFields['subscriptionId'])) {
323 $this->_setParam('subscriptionId', $responseFields['subscriptionId']);
324 }
325 }
326
327 /**
328 * @return array
329 */
330 public function _getAuthorizeNetFields() {
331 //Total amount is from the form contribution field
332 $amount = $this->_getParam('total_amount');
333 //CRM-9894 would this ever be the case??
334 if (empty($amount)) {
335 $amount = $this->_getParam('amount');
336 }
337 $fields = [];
338 $fields['x_login'] = $this->_getParam('apiLogin');
339 $fields['x_tran_key'] = $this->_getParam('paymentKey');
340 $fields['x_email_customer'] = $this->_getParam('emailCustomer');
341 $fields['x_first_name'] = $this->_getParam('billing_first_name');
342 $fields['x_last_name'] = $this->_getParam('billing_last_name');
343 $fields['x_address'] = $this->_getParam('street_address');
344 $fields['x_city'] = $this->_getParam('city');
345 $fields['x_state'] = $this->_getParam('state_province');
346 $fields['x_zip'] = $this->_getParam('postal_code');
347 $fields['x_country'] = $this->_getParam('country');
348 $fields['x_customer_ip'] = $this->_getParam('ip_address');
349 $fields['x_email'] = $this->_getParam('email');
350 $fields['x_invoice_num'] = $this->_getParam('invoiceID');
351 $fields['x_amount'] = $amount;
352 $fields['x_currency_code'] = $this->_getParam('currencyID');
353 $fields['x_description'] = $this->_getParam('description');
354 $fields['x_cust_id'] = $this->_getParam('contactID');
355 if ($this->_getParam('paymentType') == 'AIM') {
356 $fields['x_relay_response'] = 'FALSE';
357 // request response in CSV format
358 $fields['x_delim_data'] = 'TRUE';
359 $fields['x_delim_char'] = ',';
360 $fields['x_encap_char'] = '"';
361 // cc info
362 $fields['x_card_num'] = $this->_getParam('credit_card_number');
363 $fields['x_card_code'] = $this->_getParam('cvv2');
364 $exp_month = str_pad($this->_getParam('month'), 2, '0', STR_PAD_LEFT);
365 $exp_year = $this->_getParam('year');
366 $fields['x_exp_date'] = "$exp_month/$exp_year";
367 }
368
369 if ($this->_mode != 'live') {
370 $fields['x_test_request'] = 'TRUE';
371 }
372
373 return $fields;
374 }
375
376 /**
377 * Generate HMAC_MD5
378 *
379 * @param string $key
380 * @param string $data
381 *
382 * @return string
383 * the HMAC_MD5 encoding string
384 */
385 public function hmac($key, $data) {
386 if (function_exists('mhash')) {
387 // Use PHP mhash extension
388 return (bin2hex(mhash(MHASH_MD5, $data, $key)));
389 }
390 else {
391 // RFC 2104 HMAC implementation for php.
392 // Creates an md5 HMAC.
393 // Eliminates the need to install mhash to compute a HMAC
394 // Hacked by Lance Rushing
395 // byte length for md5
396 $b = 64;
397 if (strlen($key) > $b) {
398 $key = pack("H*", md5($key));
399 }
400 $key = str_pad($key, $b, chr(0x00));
401 $ipad = str_pad('', $b, chr(0x36));
402 $opad = str_pad('', $b, chr(0x5c));
403 $k_ipad = $key ^ $ipad;
404 $k_opad = $key ^ $opad;
405 return md5($k_opad . pack("H*", md5($k_ipad . $data)));
406 }
407 }
408
409 /**
410 * Calculate and return the transaction fingerprint.
411 *
412 * @return string
413 * fingerprint
414 */
415 public function CalculateFP() {
416 $x_tran_key = $this->_getParam('paymentKey');
417 $loginid = $this->_getParam('apiLogin');
418 $sequence = $this->_getParam('sequence');
419 $timestamp = $this->_getParam('timestamp');
420 $amount = $this->_getParam('amount');
421 $currency = $this->_getParam('currencyID');
422 $transaction = "$loginid^$sequence^$timestamp^$amount^$currency";
423 return $this->hmac($x_tran_key, $transaction);
424 }
425
426 /**
427 * Split a CSV file. Requires , as delimiter and " as enclosure.
428 * Based off notes from http://php.net/fgetcsv
429 *
430 * @param string $data
431 * A single CSV line.
432 *
433 * @return array
434 * CSV fields
435 */
436 public function explode_csv($data) {
437 $data = trim($data);
438 //make it easier to parse fields with quotes in them
439 $data = str_replace('""', "''", $data);
440 $fields = [];
441
442 while ($data != '') {
443 $matches = [];
444 if ($data[0] == '"') {
445 // handle quoted fields
446 preg_match('/^"(([^"]|\\")*?)",?(.*)$/', $data, $matches);
447
448 $fields[] = str_replace("''", '"', $matches[1]);
449 $data = $matches[3];
450 }
451 else {
452 preg_match('/^([^,]*),?(.*)$/', $data, $matches);
453
454 $fields[] = $matches[1];
455 $data = $matches[2];
456 }
457 }
458 return $fields;
459 }
460
461 /**
462 * Extract variables from returned XML.
463 *
464 * Function is from Authorize.Net sample code, and used
465 * to prevent the requirement of XML functions.
466 *
467 * @param string $content
468 * XML reply from Authorize.Net.
469 *
470 * @return array
471 * refId, resultCode, code, text, subscriptionId
472 */
473 public function _parseArbReturn($content) {
474 $refId = $this->_substring_between($content, '<refId>', '</refId>');
475 $resultCode = $this->_substring_between($content, '<resultCode>', '</resultCode>');
476 $code = $this->_substring_between($content, '<code>', '</code>');
477 $text = $this->_substring_between($content, '<text>', '</text>');
478 $subscriptionId = $this->_substring_between($content, '<subscriptionId>', '</subscriptionId>');
479 return [
480 'refId' => $refId,
481 'resultCode' => $resultCode,
482 'code' => $code,
483 'text' => $text,
484 'subscriptionId' => $subscriptionId,
485 ];
486 }
487
488 /**
489 * Helper function for _parseArbReturn.
490 *
491 * Function is from Authorize.Net sample code, and used to avoid using
492 * PHP5 XML functions
493 *
494 * @param string $haystack
495 * @param string $start
496 * @param string $end
497 *
498 * @return bool|string
499 */
500 public function _substring_between(&$haystack, $start, $end) {
501 if (strpos($haystack, $start) === FALSE || strpos($haystack, $end) === FALSE) {
502 return FALSE;
503 }
504 else {
505 $start_position = strpos($haystack, $start) + strlen($start);
506 $end_position = strpos($haystack, $end);
507 return substr($haystack, $start_position, $end_position - $start_position);
508 }
509 }
510
511 /**
512 * Get the value of a field if set.
513 *
514 * @param string $field
515 * The field.
516 *
517 * @param bool $xmlSafe
518 * @return mixed
519 * value of the field, or empty string if the field is
520 * not set
521 */
522 public function _getParam($field, $xmlSafe = FALSE) {
523 $value = CRM_Utils_Array::value($field, $this->_params, '');
524 if ($xmlSafe) {
525 $value = str_replace(['&', '"', "'", '<', '>'], '', $value);
526 }
527 return $value;
528 }
529
530 /**
531 * Set a field to the specified value. Value must be a scalar (int,
532 * float, string, or boolean)
533 *
534 * @param string $field
535 * @param mixed $value
536 *
537 * @return bool
538 * false if value is not a scalar, true if successful
539 */
540 public function _setParam($field, $value) {
541 if (!is_scalar($value)) {
542 return FALSE;
543 }
544 else {
545 $this->_params[$field] = $value;
546 }
547 }
548
549 /**
550 * This function checks to see if we have the right config values.
551 *
552 * @return string
553 * the error message if any
554 */
555 public function checkConfig() {
556 $error = [];
557 if (empty($this->_paymentProcessor['user_name'])) {
558 $error[] = ts('APILogin is not set for this payment processor');
559 }
560
561 if (empty($this->_paymentProcessor['password'])) {
562 $error[] = ts('Key is not set for this payment processor');
563 }
564
565 if (!empty($error)) {
566 return implode('<p>', $error);
567 }
568 else {
569 return NULL;
570 }
571 }
572
573 /**
574 * @return string
575 */
576 public function accountLoginURL() {
577 return ($this->_mode == 'test') ? 'https://test.authorize.net' : 'https://authorize.net';
578 }
579
580 /**
581 * @param string $message
582 * @param \Civi\Payment\PropertyBag $params
583 *
584 * @return bool|object
585 * @throws \Civi\Payment\Exception\PaymentProcessorException
586 */
587 public function cancelSubscription(&$message = '', $params) {
588 $template = CRM_Core_Smarty::singleton();
589
590 $template->assign('subscriptionType', 'cancel');
591
592 $template->assign('apiLogin', $this->_getParam('apiLogin'));
593 $template->assign('paymentKey', $this->_getParam('paymentKey'));
594 $template->assign('subscriptionId', $params->getRecurProcessorID());
595
596 $arbXML = $template->fetch('CRM/Contribute/Form/Contribution/AuthorizeNetARB.tpl');
597
598 $response = (string) $this->getGuzzleClient()->post($this->_paymentProcessor['url_recur'], [
599 'headers' => [
600 'Content-Type' => 'text/xml; charset=UTF8',
601 ],
602 'body' => $arbXML,
603 'curl' => [
604 CURLOPT_RETURNTRANSFER => TRUE,
605 CURLOPT_SSL_VERIFYPEER => Civi::settings()->get('verifySSL'),
606 ],
607 ])->getBody();
608
609 $responseFields = $this->_ParseArbReturn($response);
610 $message = "{$responseFields['code']}: {$responseFields['text']}";
611
612 if ($responseFields['resultCode'] === 'Error') {
613 throw new PaymentProcessorException($responseFields['text'], $responseFields['code']);
614 }
615 return TRUE;
616 }
617
618 /**
619 * Update payment details at Authorize.net.
620 *
621 * @param string $message
622 * @param array $params
623 *
624 * @return bool|object
625 *
626 * @throws \Civi\Payment\Exception\PaymentProcessorException
627 */
628 public function updateSubscriptionBillingInfo(&$message = '', $params = []) {
629 $template = CRM_Core_Smarty::singleton();
630 $template->assign('subscriptionType', 'updateBilling');
631
632 $template->assign('apiLogin', $this->_getParam('apiLogin'));
633 $template->assign('paymentKey', $this->_getParam('paymentKey'));
634 $template->assign('subscriptionId', $params['subscriptionId']);
635
636 $template->assign('cardNumber', $params['credit_card_number']);
637 $exp_month = str_pad($params['month'], 2, '0', STR_PAD_LEFT);
638 $exp_year = $params['year'];
639 $template->assign('expirationDate', $exp_year . '-' . $exp_month);
640
641 $template->assign('billingFirstName', $params['first_name']);
642 $template->assign('billingLastName', $params['last_name']);
643 $template->assign('billingAddress', $params['street_address']);
644 $template->assign('billingCity', $params['city']);
645 $template->assign('billingState', $params['state_province']);
646 $template->assign('billingZip', $params['postal_code']);
647 $template->assign('billingCountry', $params['country']);
648
649 $arbXML = $template->fetch('CRM/Contribute/Form/Contribution/AuthorizeNetARB.tpl');
650
651 $response = (string) $this->getGuzzleClient()->post($this->_paymentProcessor['url_recur'], [
652 'headers' => [
653 'Content-Type' => 'text/xml; charset=UTF8',
654 ],
655 'body' => $arbXML,
656 'curl' => [
657 CURLOPT_RETURNTRANSFER => TRUE,
658 CURLOPT_SSL_VERIFYPEER => Civi::settings()->get('verifySSL'),
659 ],
660 ])->getBody();
661
662 // submit to authorize.net
663 $responseFields = $this->_ParseArbReturn($response);
664 $message = "{$responseFields['code']}: {$responseFields['text']}";
665
666 if ($responseFields['resultCode'] === 'Error') {
667 throw new PaymentProcessorException($responseFields['text'], $responseFields['code']);
668 }
669 return TRUE;
670 }
671
672 /**
673 * Process incoming notification.
674 */
675 public function handlePaymentNotification() {
676 $ipnClass = new CRM_Core_Payment_AuthorizeNetIPN(array_merge($_GET, $_REQUEST));
677 $ipnClass->main();
678 }
679
680 /**
681 * Change the amount of the recurring payment.
682 *
683 * @param string $message
684 * @param array $params
685 *
686 * @return bool|object
687 *
688 * @throws \Civi\Payment\Exception\PaymentProcessorException
689 */
690 public function changeSubscriptionAmount(&$message = '', $params = []) {
691 $template = CRM_Core_Smarty::singleton();
692
693 $template->assign('subscriptionType', 'update');
694
695 $template->assign('apiLogin', $this->_getParam('apiLogin'));
696 $template->assign('paymentKey', $this->_getParam('paymentKey'));
697
698 $template->assign('subscriptionId', $params['subscriptionId']);
699
700 // for open ended subscription totalOccurrences has to be 9999
701 $installments = empty($params['installments']) ? 9999 : $params['installments'];
702 $template->assign('totalOccurrences', $installments);
703
704 $template->assign('amount', $params['amount']);
705
706 $arbXML = $template->fetch('CRM/Contribute/Form/Contribution/AuthorizeNetARB.tpl');
707
708 $response = (string) $this->getGuzzleClient()->post($this->_paymentProcessor['url_recur'], [
709 'headers' => [
710 'Content-Type' => 'text/xml; charset=UTF8',
711 ],
712 'body' => $arbXML,
713 'curl' => [
714 CURLOPT_RETURNTRANSFER => TRUE,
715 CURLOPT_SSL_VERIFYPEER => Civi::settings()->get('verifySSL'),
716 ],
717 ])->getBody();
718
719 $responseFields = $this->_parseArbReturn($response);
720 $message = "{$responseFields['code']}: {$responseFields['text']}";
721
722 if ($responseFields['resultCode'] === 'Error') {
723 throw new PaymentProcessorException($responseFields['text'], $responseFields['code']);
724 }
725 return TRUE;
726 }
727
728 /**
729 * Get an appropriate test trannsaction id.
730 *
731 * @return string
732 */
733 protected function getTestTrxnID() {
734 // test mode always returns trxn_id = 0
735 // also live mode in CiviCRM with test mode set in
736 // Authorize.Net return $response_fields[6] = 0
737 // hence treat that also as test mode transaction
738 // fix for CRM-2566
739 $query = "SELECT MAX(trxn_id) FROM civicrm_contribution WHERE trxn_id RLIKE 'test[0-9]+'";
740 $trxn_id = (string) (CRM_Core_DAO::singleValueQuery($query));
741 $trxn_id = str_replace('test', '', $trxn_id);
742 $trxn_id = (int) ($trxn_id) + 1;
743 return sprintf('test%08d', $trxn_id);
744 }
745
746 }