Merge pull request #23262 from eileenmcnaughton/limit
[civicrm-core.git] / CRM / Core / Payment / AuthorizeNet.php
CommitLineData
6a488035
TO
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
4ae44cc6
RLAR
16use Civi\Payment\Exception\PaymentProcessorException;
17
c866eb5f
TO
18/**
19 * NOTE:
6a488035
TO
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 */
23class CRM_Core_Payment_AuthorizeNet extends CRM_Core_Payment {
7da04cde
TO
24 const CHARSET = 'iso-8859-1';
25 const AUTH_APPROVED = 1;
26 const AUTH_DECLINED = 2;
27 const AUTH_ERROR = 3;
16106615 28 const AUTH_REVIEW = 4;
7da04cde 29 const TIMEZONE = 'America/Denver';
6a488035
TO
30
31 protected $_mode = NULL;
32
be2fb01f 33 protected $_params = [];
6a488035 34
9fa25593 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
6a488035 54 /**
fe482240 55 * Constructor.
6a488035 56 *
6a0b768e
TO
57 * @param string $mode
58 * The mode of operation: live or test.
6a488035 59 *
77b97be7
EM
60 * @param $paymentProcessor
61 *
62 * @return \CRM_Core_Payment_AuthorizeNet
6a488035 63 */
00be9182 64 public function __construct($mode, &$paymentProcessor) {
6a488035
TO
65 $this->_mode = $mode;
66 $this->_paymentProcessor = $paymentProcessor;
6a488035 67
6a488035
TO
68 $this->_setParam('apiLogin', $paymentProcessor['user_name']);
69 $this->_setParam('paymentKey', $paymentProcessor['password']);
70 $this->_setParam('paymentType', 'AIM');
6a488035
TO
71 }
72
fbcb6fba 73 /**
d09edf64 74 * Should the first payment date be configurable when setting up back office recurring payments.
fbcb6fba
EM
75 * In the case of Authorize.net this is an option
76 * @return bool
77 */
d8ce0d68 78 protected function supportsFutureRecurStartDate() {
fbcb6fba
EM
79 return TRUE;
80 }
81
677fe56c
EM
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
6a488035 98 /**
fe482240 99 * Submit a payment using Advanced Integration Method.
6a488035 100 *
5d8d2f38
MW
101 * @param array|\Civi\Payment\PropertyBag $params
102 *
103 * @param string $component
6a488035 104 *
a6c01b45 105 * @return array
5d8d2f38 106 * Result array (containing at least the key payment_status_id)
9fa25593 107 *
108 * @throws \Civi\Payment\Exception\PaymentProcessorException
6a488035 109 */
5d8d2f38
MW
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
6a488035 124 if (!defined('CURLOPT_SSLCERT')) {
5aa04455 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);
6a488035
TO
128 }
129
130 /*
b44e3f84 131 * recurpayment function does not compile an array & then process it -
6a488035
TO
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;
cd125a40 136 if (!empty($params['is_recur']) && !empty($params['contributionRecurID'])) {
6a488035
TO
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
cd125a40 146 if (!empty($params['is_recur']) && !empty($params['contributionRecurID'])) {
2edc95e6 147 $this->doRecurPayment();
5d8d2f38
MW
148 $params['payment_status_id'] = array_search('Pending', $statuses);
149 $params['payment_status'] = 'Pending';
6a488035
TO
150 return $params;
151 }
152
be2fb01f 153 $postFields = [];
6a488035
TO
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
74fd8b01 170 if ($this->checkDupe($authorizeNetFields['x_invoice_num'], $params['contributionID'] ?? NULL)) {
39fef483 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);
6a488035
TO
172 }
173
5aa04455 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();
6a488035
TO
181
182 $response_fields = $this->explode_csv($response);
48e3da59 183
184 // fetch available contribution statuses
185 $contributionStatus = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
186
a1278f71 187 $result = [];
6a488035
TO
188 // check for application errors
189 // TODO:
190 // AVS, CVV2, CAVV, and other verification results
16106615 191 switch ($response_fields[0]) {
d167bea8 192 case self::AUTH_REVIEW:
a1278f71 193 $result = $this->setStatusPaymentPending($result);
16106615 194 break;
195
d167bea8 196 case self::AUTH_ERROR:
d15a29b5 197 $errormsg = $response_fields[2] . ' ' . $response_fields[3];
39fef483 198 throw new PaymentProcessorException($errormsg, $response_fields[1]);
16106615 199
d167bea8 200 case self::AUTH_DECLINED:
16106615 201 $errormsg = $response_fields[2] . ' ' . $response_fields[3];
39fef483 202 throw new PaymentProcessorException($errormsg, $response_fields[1]);
16106615 203
204 default:
205 // Success
a1278f71
MW
206 $result['trxn_id'] = !empty($response_fields[6]) ? $response_fields[6] : $this->getTestTrxnID();
207 $result = $this->setStatusPaymentCompleted($result);
16106615 208 break;
6a488035 209 }
524cd99e 210
a1278f71 211 return $result;
6a488035
TO
212 }
213
214 /**
fe482240 215 * Submit an Automated Recurring Billing subscription.
6a488035 216 */
00be9182 217 public function doRecurPayment() {
6a488035
TO
218 $template = CRM_Core_Smarty::singleton();
219
220 $intervalLength = $this->_getParam('frequency_interval');
221 $intervalUnit = $this->_getParam('frequency_unit');
2edc95e6 222 if ($intervalUnit === 'week') {
6a488035
TO
223 $intervalLength *= 7;
224 $intervalUnit = 'days';
225 }
2edc95e6 226 elseif ($intervalUnit === 'year') {
6a488035
TO
227 $intervalLength *= 12;
228 $intervalUnit = 'months';
229 }
31c873bf 230 elseif ($intervalUnit === 'day') {
6a488035 231 $intervalUnit = 'days';
2edc95e6 232 // interval cannot be less than 7 days or more than 1 year
6a488035 233 if ($intervalLength < 7) {
2edc95e6 234 throw new PaymentProcessorException('Payment interval must be at least one week', 9001);
6a488035 235 }
2edc95e6 236 if ($intervalLength > 365) {
237 throw new PaymentProcessorException('Payment interval may not be longer than one year', 9001);
6a488035
TO
238 }
239 }
2edc95e6 240 elseif ($intervalUnit === 'month') {
241 $intervalUnit = 'months';
6a488035 242 if ($intervalLength < 1) {
2edc95e6 243 throw new PaymentProcessorException('Payment interval must be at least one week', 9001);
6a488035 244 }
2edc95e6 245 if ($intervalLength > 12) {
246 throw new PaymentProcessorException('Payment interval may not be longer than one year', 9001);
6a488035
TO
247 }
248 }
249
250 $template->assign('intervalLength', $intervalLength);
251 $template->assign('intervalUnit', $intervalUnit);
252
253 $template->assign('apiLogin', $this->_getParam('apiLogin'));
254 $template->assign('paymentKey', $this->_getParam('paymentKey'));
255 $template->assign('refId', substr($this->_getParam('invoiceID'), 0, 20));
256
257 //for recurring, carry first contribution id
258 $template->assign('invoiceNumber', $this->_getParam('contributionID'));
259 $firstPaymentDate = $this->_getParam('receive_date');
260 if (!empty($firstPaymentDate)) {
261 //allow for post dated payment if set in form
262 $startDate = date_create($firstPaymentDate);
263 }
264 else {
265 $startDate = date_create();
266 }
267 /* Format start date in Mountain Time to avoid Authorize.net error E00017
268 * we do this only if the day we are setting our start time to is LESS than the current
269 * day in mountaintime (ie. the server time of the A-net server). A.net won't accept a date
270 * earlier than the current date on it's server so if we are in PST we might need to use mountain
271 * time to bring our date forward. But if we are submitting something future dated we want
272 * the date we entered to be respected
273 */
274 $minDate = date_create('now', new DateTimeZone(self::TIMEZONE));
9b873358 275 if (strtotime($startDate->format('Y-m-d')) < strtotime($minDate->format('Y-m-d'))) {
6a488035
TO
276 $startDate->setTimezone(new DateTimeZone(self::TIMEZONE));
277 }
278
481a74f4 279 $template->assign('startDate', $startDate->format('Y-m-d'));
a2dd09cc 280
6a488035 281 $installments = $this->_getParam('installments');
a2dd09cc
DL
282
283 // for open ended subscription totalOccurrences has to be 9999
284 $installments = empty($installments) ? 9999 : $installments;
285 $template->assign('totalOccurrences', $installments);
6a488035
TO
286
287 $template->assign('amount', $this->_getParam('amount'));
288
289 $template->assign('cardNumber', $this->_getParam('credit_card_number'));
290 $exp_month = str_pad($this->_getParam('month'), 2, '0', STR_PAD_LEFT);
291 $exp_year = $this->_getParam('year');
292 $template->assign('expirationDate', $exp_year . '-' . $exp_month);
293
294 // name rather than description is used in the tpl - see http://www.authorize.net/support/ARB_guide.pdf
295 $template->assign('name', $this->_getParam('description', TRUE));
296
297 $template->assign('email', $this->_getParam('email'));
298 $template->assign('contactID', $this->_getParam('contactID'));
299 $template->assign('billingFirstName', $this->_getParam('billing_first_name'));
300 $template->assign('billingLastName', $this->_getParam('billing_last_name'));
301 $template->assign('billingAddress', $this->_getParam('street_address', TRUE));
302 $template->assign('billingCity', $this->_getParam('city', TRUE));
303 $template->assign('billingState', $this->_getParam('state_province'));
304 $template->assign('billingZip', $this->_getParam('postal_code', TRUE));
305 $template->assign('billingCountry', $this->_getParam('country'));
fc219888
EM
306 // Required to be set for s
307 $template->ensureVariablesAreAssigned(['subscriptionType']);
6a488035 308 $arbXML = $template->fetch('CRM/Contribute/Form/Contribution/AuthorizeNetARB.tpl');
6a488035 309
9fa25593 310 // Submit to authorize.net
311 $response = $this->getGuzzleClient()->post($this->_paymentProcessor['url_recur'], [
312 'headers' => [
313 'Content-Type' => 'text/xml; charset=UTF8',
314 ],
315 'body' => $arbXML,
316 'curl' => [
317 CURLOPT_RETURNTRANSFER => TRUE,
318 CURLOPT_SSL_VERIFYPEER => Civi::settings()->get('verifySSL'),
319 ],
320 ]);
321 $responseFields = $this->_ParseArbReturn((string) $response->getBody());
322
323 if ($responseFields['resultCode'] === 'Error') {
884a213d 324 throw new PaymentProcessorException($responseFields['text'], $responseFields['code']);
6a488035
TO
325 }
326
327 // update recur processor_id with subscriptionId
328 CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_ContributionRecur', $this->_getParam('contributionRecurID'),
329 'processor_id', $responseFields['subscriptionId']
330 );
331 //only impact of assigning this here is is can be used to cancel the subscription in an automated test
332 // if it isn't cancelled a duplicate transaction error occurs
a7488080 333 if (!empty($responseFields['subscriptionId'])) {
6a488035
TO
334 $this->_setParam('subscriptionId', $responseFields['subscriptionId']);
335 }
336 }
337
6c786a9b
EM
338 /**
339 * @return array
340 */
00be9182 341 public function _getAuthorizeNetFields() {
518fa0ee
SL
342 //Total amount is from the form contribution field
343 $amount = $this->_getParam('total_amount');
344 //CRM-9894 would this ever be the case??
345 if (empty($amount)) {
6a488035
TO
346 $amount = $this->_getParam('amount');
347 }
be2fb01f 348 $fields = [];
6a488035
TO
349 $fields['x_login'] = $this->_getParam('apiLogin');
350 $fields['x_tran_key'] = $this->_getParam('paymentKey');
351 $fields['x_email_customer'] = $this->_getParam('emailCustomer');
352 $fields['x_first_name'] = $this->_getParam('billing_first_name');
353 $fields['x_last_name'] = $this->_getParam('billing_last_name');
354 $fields['x_address'] = $this->_getParam('street_address');
355 $fields['x_city'] = $this->_getParam('city');
356 $fields['x_state'] = $this->_getParam('state_province');
357 $fields['x_zip'] = $this->_getParam('postal_code');
358 $fields['x_country'] = $this->_getParam('country');
359 $fields['x_customer_ip'] = $this->_getParam('ip_address');
360 $fields['x_email'] = $this->_getParam('email');
5bd23b41 361 $fields['x_invoice_num'] = $this->_getParam('invoiceID');
353ffa53 362 $fields['x_amount'] = $amount;
6a488035
TO
363 $fields['x_currency_code'] = $this->_getParam('currencyID');
364 $fields['x_description'] = $this->_getParam('description');
365 $fields['x_cust_id'] = $this->_getParam('contactID');
366 if ($this->_getParam('paymentType') == 'AIM') {
367 $fields['x_relay_response'] = 'FALSE';
368 // request response in CSV format
369 $fields['x_delim_data'] = 'TRUE';
370 $fields['x_delim_char'] = ',';
371 $fields['x_encap_char'] = '"';
372 // cc info
373 $fields['x_card_num'] = $this->_getParam('credit_card_number');
374 $fields['x_card_code'] = $this->_getParam('cvv2');
375 $exp_month = str_pad($this->_getParam('month'), 2, '0', STR_PAD_LEFT);
376 $exp_year = $this->_getParam('year');
377 $fields['x_exp_date'] = "$exp_month/$exp_year";
378 }
379
380 if ($this->_mode != 'live') {
381 $fields['x_test_request'] = 'TRUE';
382 }
383
384 return $fields;
385 }
386
6a488035
TO
387 /**
388 * Split a CSV file. Requires , as delimiter and " as enclosure.
389 * Based off notes from http://php.net/fgetcsv
390 *
6a0b768e
TO
391 * @param string $data
392 * A single CSV line.
6a488035 393 *
a6c01b45
CW
394 * @return array
395 * CSV fields
6a488035 396 */
00be9182 397 public function explode_csv($data) {
6a488035
TO
398 $data = trim($data);
399 //make it easier to parse fields with quotes in them
400 $data = str_replace('""', "''", $data);
be2fb01f 401 $fields = [];
6a488035
TO
402
403 while ($data != '') {
be2fb01f 404 $matches = [];
6a488035
TO
405 if ($data[0] == '"') {
406 // handle quoted fields
407 preg_match('/^"(([^"]|\\")*?)",?(.*)$/', $data, $matches);
408
409 $fields[] = str_replace("''", '"', $matches[1]);
410 $data = $matches[3];
411 }
412 else {
413 preg_match('/^([^,]*),?(.*)$/', $data, $matches);
414
415 $fields[] = $matches[1];
416 $data = $matches[2];
417 }
418 }
419 return $fields;
420 }
421
422 /**
fe482240 423 * Extract variables from returned XML.
6a488035
TO
424 *
425 * Function is from Authorize.Net sample code, and used
426 * to prevent the requirement of XML functions.
427 *
6a0b768e
TO
428 * @param string $content
429 * XML reply from Authorize.Net.
6a488035 430 *
a6c01b45
CW
431 * @return array
432 * refId, resultCode, code, text, subscriptionId
6a488035 433 */
00be9182 434 public function _parseArbReturn($content) {
353ffa53
TO
435 $refId = $this->_substring_between($content, '<refId>', '</refId>');
436 $resultCode = $this->_substring_between($content, '<resultCode>', '</resultCode>');
437 $code = $this->_substring_between($content, '<code>', '</code>');
438 $text = $this->_substring_between($content, '<text>', '</text>');
6a488035 439 $subscriptionId = $this->_substring_between($content, '<subscriptionId>', '</subscriptionId>');
be2fb01f 440 return [
6a488035
TO
441 'refId' => $refId,
442 'resultCode' => $resultCode,
443 'code' => $code,
444 'text' => $text,
445 'subscriptionId' => $subscriptionId,
be2fb01f 446 ];
6a488035
TO
447 }
448
449 /**
fe482240 450 * Helper function for _parseArbReturn.
6a488035
TO
451 *
452 * Function is from Authorize.Net sample code, and used to avoid using
453 * PHP5 XML functions
54957108 454 *
455 * @param string $haystack
456 * @param string $start
457 * @param string $end
458 *
459 * @return bool|string
6a488035 460 */
00be9182 461 public function _substring_between(&$haystack, $start, $end) {
6a488035
TO
462 if (strpos($haystack, $start) === FALSE || strpos($haystack, $end) === FALSE) {
463 return FALSE;
464 }
465 else {
466 $start_position = strpos($haystack, $start) + strlen($start);
467 $end_position = strpos($haystack, $end);
468 return substr($haystack, $start_position, $end_position - $start_position);
469 }
470 }
471
472 /**
fe482240 473 * Get the value of a field if set.
6a488035 474 *
6a0b768e
TO
475 * @param string $field
476 * The field.
6a488035 477 *
2a6da8d7 478 * @param bool $xmlSafe
72b3a70c
CW
479 * @return mixed
480 * value of the field, or empty string if the field is
16b10e64 481 * not set
6a488035 482 */
00be9182 483 public function _getParam($field, $xmlSafe = FALSE) {
6a488035 484 $value = CRM_Utils_Array::value($field, $this->_params, '');
a2dd09cc 485 if ($xmlSafe) {
be2fb01f 486 $value = str_replace(['&', '"', "'", '<', '>'], '', $value);
a2dd09cc 487 }
6a488035
TO
488 return $value;
489 }
490
6a488035
TO
491 /**
492 * Set a field to the specified value. Value must be a scalar (int,
493 * float, string, or boolean)
494 *
495 * @param string $field
496 * @param mixed $value
497 *
a6c01b45
CW
498 * @return bool
499 * false if value is not a scalar, true if successful
6a488035 500 */
00be9182 501 public function _setParam($field, $value) {
6a488035
TO
502 if (!is_scalar($value)) {
503 return FALSE;
504 }
505 else {
506 $this->_params[$field] = $value;
507 }
508 }
509
510 /**
fe482240 511 * This function checks to see if we have the right config values.
6a488035 512 *
a6c01b45
CW
513 * @return string
514 * the error message if any
6a488035 515 */
00be9182 516 public function checkConfig() {
be2fb01f 517 $error = [];
6a488035
TO
518 if (empty($this->_paymentProcessor['user_name'])) {
519 $error[] = ts('APILogin is not set for this payment processor');
520 }
521
522 if (empty($this->_paymentProcessor['password'])) {
523 $error[] = ts('Key is not set for this payment processor');
524 }
525
526 if (!empty($error)) {
527 return implode('<p>', $error);
528 }
529 else {
530 return NULL;
531 }
532 }
533
6c786a9b
EM
534 /**
535 * @return string
536 */
00be9182 537 public function accountLoginURL() {
6a488035
TO
538 return ($this->_mode == 'test') ? 'https://test.authorize.net' : 'https://authorize.net';
539 }
540
6c786a9b
EM
541 /**
542 * @param string $message
9e03a6d0 543 * @param \Civi\Payment\PropertyBag $params
6c786a9b
EM
544 *
545 * @return bool|object
884a213d 546 * @throws \Civi\Payment\Exception\PaymentProcessorException
6c786a9b 547 */
ff708ff0 548 public function cancelSubscription(&$message = '', $params = []) {
6a488035
TO
549 $template = CRM_Core_Smarty::singleton();
550
551 $template->assign('subscriptionType', 'cancel');
552
553 $template->assign('apiLogin', $this->_getParam('apiLogin'));
554 $template->assign('paymentKey', $this->_getParam('paymentKey'));
9e03a6d0 555 $template->assign('subscriptionId', $params->getRecurProcessorID());
6a488035
TO
556
557 $arbXML = $template->fetch('CRM/Contribute/Form/Contribution/AuthorizeNetARB.tpl');
558
884a213d 559 $response = (string) $this->getGuzzleClient()->post($this->_paymentProcessor['url_recur'], [
560 'headers' => [
561 'Content-Type' => 'text/xml; charset=UTF8',
562 ],
563 'body' => $arbXML,
564 'curl' => [
565 CURLOPT_RETURNTRANSFER => TRUE,
566 CURLOPT_SSL_VERIFYPEER => Civi::settings()->get('verifySSL'),
567 ],
568 ])->getBody();
6a488035
TO
569
570 $responseFields = $this->_ParseArbReturn($response);
571 $message = "{$responseFields['code']}: {$responseFields['text']}";
572
884a213d 573 if ($responseFields['resultCode'] === 'Error') {
574 throw new PaymentProcessorException($responseFields['text'], $responseFields['code']);
6a488035
TO
575 }
576 return TRUE;
577 }
578
6c786a9b 579 /**
884a213d 580 * Update payment details at Authorize.net.
581 *
6c786a9b
EM
582 * @param string $message
583 * @param array $params
584 *
585 * @return bool|object
884a213d 586 *
587 * @throws \Civi\Payment\Exception\PaymentProcessorException
6c786a9b 588 */
be2fb01f 589 public function updateSubscriptionBillingInfo(&$message = '', $params = []) {
6a488035
TO
590 $template = CRM_Core_Smarty::singleton();
591 $template->assign('subscriptionType', 'updateBilling');
592
593 $template->assign('apiLogin', $this->_getParam('apiLogin'));
594 $template->assign('paymentKey', $this->_getParam('paymentKey'));
595 $template->assign('subscriptionId', $params['subscriptionId']);
596
597 $template->assign('cardNumber', $params['credit_card_number']);
598 $exp_month = str_pad($params['month'], 2, '0', STR_PAD_LEFT);
599 $exp_year = $params['year'];
600 $template->assign('expirationDate', $exp_year . '-' . $exp_month);
601
602 $template->assign('billingFirstName', $params['first_name']);
603 $template->assign('billingLastName', $params['last_name']);
604 $template->assign('billingAddress', $params['street_address']);
605 $template->assign('billingCity', $params['city']);
606 $template->assign('billingState', $params['state_province']);
607 $template->assign('billingZip', $params['postal_code']);
608 $template->assign('billingCountry', $params['country']);
609
610 $arbXML = $template->fetch('CRM/Contribute/Form/Contribution/AuthorizeNetARB.tpl');
611
884a213d 612 $response = (string) $this->getGuzzleClient()->post($this->_paymentProcessor['url_recur'], [
613 'headers' => [
614 'Content-Type' => 'text/xml; charset=UTF8',
615 ],
616 'body' => $arbXML,
617 'curl' => [
618 CURLOPT_RETURNTRANSFER => TRUE,
619 CURLOPT_SSL_VERIFYPEER => Civi::settings()->get('verifySSL'),
620 ],
621 ])->getBody();
6a488035 622
884a213d 623 // submit to authorize.net
6a488035
TO
624 $responseFields = $this->_ParseArbReturn($response);
625 $message = "{$responseFields['code']}: {$responseFields['text']}";
626
884a213d 627 if ($responseFields['resultCode'] === 'Error') {
628 throw new PaymentProcessorException($responseFields['text'], $responseFields['code']);
6a488035
TO
629 }
630 return TRUE;
631 }
632
23de1ac0
EM
633 /**
634 * Process incoming notification.
635 */
9645e182 636 public function handlePaymentNotification() {
23de1ac0
EM
637 $ipnClass = new CRM_Core_Payment_AuthorizeNetIPN(array_merge($_GET, $_REQUEST));
638 $ipnClass->main();
639 }
640
6c786a9b 641 /**
884a213d 642 * Change the amount of the recurring payment.
643 *
6c786a9b
EM
644 * @param string $message
645 * @param array $params
646 *
647 * @return bool|object
884a213d 648 *
649 * @throws \Civi\Payment\Exception\PaymentProcessorException
6c786a9b 650 */
be2fb01f 651 public function changeSubscriptionAmount(&$message = '', $params = []) {
6a488035
TO
652 $template = CRM_Core_Smarty::singleton();
653
654 $template->assign('subscriptionType', 'update');
655
656 $template->assign('apiLogin', $this->_getParam('apiLogin'));
657 $template->assign('paymentKey', $this->_getParam('paymentKey'));
658
659 $template->assign('subscriptionId', $params['subscriptionId']);
702c1203
JM
660
661 // for open ended subscription totalOccurrences has to be 9999
662 $installments = empty($params['installments']) ? 9999 : $params['installments'];
663 $template->assign('totalOccurrences', $installments);
664
6a488035
TO
665 $template->assign('amount', $params['amount']);
666
667 $arbXML = $template->fetch('CRM/Contribute/Form/Contribution/AuthorizeNetARB.tpl');
668
884a213d 669 $response = (string) $this->getGuzzleClient()->post($this->_paymentProcessor['url_recur'], [
670 'headers' => [
671 'Content-Type' => 'text/xml; charset=UTF8',
672 ],
673 'body' => $arbXML,
674 'curl' => [
675 CURLOPT_RETURNTRANSFER => TRUE,
676 CURLOPT_SSL_VERIFYPEER => Civi::settings()->get('verifySSL'),
677 ],
678 ])->getBody();
6a488035 679
6ea16bbf 680 $responseFields = $this->_parseArbReturn($response);
6a488035
TO
681 $message = "{$responseFields['code']}: {$responseFields['text']}";
682
884a213d 683 if ($responseFields['resultCode'] === 'Error') {
4ae44cc6 684 throw new PaymentProcessorException($responseFields['text'], $responseFields['code']);
6a488035
TO
685 }
686 return TRUE;
687 }
96025800 688
524cd99e 689 /**
690 * Get an appropriate test trannsaction id.
691 *
692 * @return string
693 */
694 protected function getTestTrxnID() {
695 // test mode always returns trxn_id = 0
696 // also live mode in CiviCRM with test mode set in
697 // Authorize.Net return $response_fields[6] = 0
698 // hence treat that also as test mode transaction
699 // fix for CRM-2566
700 $query = "SELECT MAX(trxn_id) FROM civicrm_contribution WHERE trxn_id RLIKE 'test[0-9]+'";
701 $trxn_id = (string) (CRM_Core_DAO::singleValueQuery($query));
702 $trxn_id = str_replace('test', '', $trxn_id);
703 $trxn_id = (int) ($trxn_id) + 1;
704 return sprintf('test%08d', $trxn_id);
705 }
706
6a488035 707}