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