Merge pull request #24148 from civicrm/5.52
[civicrm-core.git] / CRM / Core / Payment / Realex.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
f452d72c 4 | Copyright CiviCRM LLC. All rights reserved. |
6a488035 5 | |
f452d72c
CW
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
6a488035 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
6a488035 11
6a488035 12/*
d424ffde
CW
13 * Copyright (C) 2009
14 * Licensed to CiviCRM under the Academic Free License version 3.0.
15 *
16 * Written and contributed by Kirkdesigns (http://www.kirkdesigns.co.uk)
17 */
6a488035 18
9248c002 19use Civi\Payment\Exception\PaymentProcessorException;
20
6a488035
TO
21/**
22 *
23 * @package CRM
24 * @author Tom Kirkpatrick <tkp@kirkdesigns.co.uk>
6a488035
TO
25 */
26class CRM_Core_Payment_Realex extends CRM_Core_Payment {
7da04cde 27 const AUTH_APPROVED = '00';
6a488035 28
9248c002 29 protected $_mode;
6a488035 30
be2fb01f 31 protected $_params = [];
6a488035 32
6a488035 33 /**
fe482240 34 * Constructor.
6a488035 35 *
6a0b768e
TO
36 * @param string $mode
37 * The mode of operation: live or test.
6a488035 38 *
9248c002 39 * @param array $paymentProcessor
6a488035 40 */
00be9182 41 public function __construct($mode, &$paymentProcessor) {
6a488035
TO
42 $this->_mode = $mode;
43 $this->_paymentProcessor = $paymentProcessor;
6a488035
TO
44
45 $this->_setParam('merchant_ref', $paymentProcessor['user_name']);
46 $this->_setParam('secret', $paymentProcessor['password']);
47 $this->_setParam('account', $paymentProcessor['subject']);
48
49 $this->_setParam('emailCustomer', 'TRUE');
50 srand(time());
51 $this->_setParam('sequence', rand(1, 1000));
52 }
6a488035 53
6a488035 54 /**
fe482240 55 * Submit a payment using Advanced Integration Method.
6a488035 56 *
2d9ca642
MW
57 * @param array|PropertyBag $params
58 *
59 * @param string $component
6a488035 60 *
a6c01b45 61 * @return array
2d9ca642 62 * Result array (containing at least the key payment_status_id)
9248c002 63 *
64 * @throws \Civi\Payment\Exception\PaymentProcessorException
6a488035 65 */
2d9ca642
MW
66 public function doPayment(&$params, $component = 'contribute') {
67 $propertyBag = \Civi\Payment\PropertyBag::cast($params);
68 $this->_component = $component;
8e819886 69 $result = $this->setStatusPaymentPending([]);
2d9ca642
MW
70
71 // If we have a $0 amount, skip call to processor and set payment_status to Completed.
72 // Conceivably a processor might override this - perhaps for setting up a token - but we don't
73 // have an example of that at the moment.
74 if ($propertyBag->getAmount() == 0) {
8e819886 75 $result = $this->setStatusPaymentCompleted($result);
2d9ca642
MW
76 return $result;
77 }
6a488035
TO
78
79 if (!defined('CURLOPT_SSLCERT')) {
9248c002 80 throw new PaymentProcessorException(ts('RealAuth requires curl with SSL support'), 9001);
6a488035
TO
81 }
82
8e819886 83 $this->setRealexFields($params);
6a488035 84
6a488035
TO
85 /**********************************************************
86 * Check to see if we have a duplicate before we send
87 **********************************************************/
d253aeb8 88 if ($this->checkDupe($params['invoiceID'], CRM_Utils_Array::value('contributionID', $params))) {
9248c002 89 throw new PaymentProcessorException(ts('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
90 }
91
92 // Create sha1 hash for request
353ffa53 93 $hashme = "{$this->_getParam('timestamp')}.{$this->_getParam('merchant_ref')}.{$this->_getParam('order_id')}.{$this->_getParam('amount')}.{$this->_getParam('currency')}.{$this->_getParam('card_number')}";
6a488035 94 $sha1hash = sha1($hashme);
353ffa53 95 $hashme = "$sha1hash.{$this->_getParam('secret')}";
6a488035
TO
96 $sha1hash = sha1($hashme);
97
6a488035
TO
98 // Generate the request xml that is send to Realex Payments.
99 $request_xml = "<request type='auth' timestamp='{$this->_getParam('timestamp')}'>
100 <merchantid>{$this->_getParam('merchant_ref')}</merchantid>
101 <account>{$this->_getParam('account')}</account>
102 <orderid>{$this->_getParam('order_id')}</orderid>
103 <amount currency='{$this->_getParam('currency')}'>{$this->_getParam('amount')}</amount>
104 <card>
105 <number>{$this->_getParam('card_number')}</number>
106 <expdate>{$this->_getParam('exp_date')}</expdate>
107 <type>{$this->_getParam('card_type')}</type>
108 <chname>{$this->_getParam('card_name')}</chname>
109 <issueno>{$this->_getParam('issue_number')}</issueno>
110 <cvn>
111 <number>{$this->_getParam('cvn')}</number>
112 <presind>1</presind>
113 </cvn>
114 </card>
115 <autosettle flag='1'/>
116 <sha1hash>$sha1hash</sha1hash>
117 <comments>
118 <comment id='1'>{$this->_getParam('comments')}</comment>
119 </comments>
120 <tssinfo>
121 <varref>{$this->_getParam('varref')}</varref>
122 </tssinfo>
123 </request>";
124
125 /**********************************************************
126 * Send to the payment processor using cURL
127 **********************************************************/
128
129 $submit = curl_init($this->_paymentProcessor['url_site']);
130
131 if (!$submit) {
9248c002 132 throw new PaymentProcessorException(ts('Could not initiate connection to payment gateway'), 9002);
6a488035
TO
133 }
134
be2fb01f 135 curl_setopt($submit, CURLOPT_HTTPHEADER, ['SOAPAction: ""']);
6a488035
TO
136 curl_setopt($submit, CURLOPT_RETURNTRANSFER, 1);
137 curl_setopt($submit, CURLOPT_TIMEOUT, 60);
aaffa79f 138 curl_setopt($submit, CURLOPT_SSL_VERIFYPEER, Civi::settings()->get('verifySSL'));
6a488035
TO
139 curl_setopt($submit, CURLOPT_HEADER, 0);
140
141 // take caching out of the picture
142 curl_setopt($submit, CURLOPT_FORBID_REUSE, 1);
143 curl_setopt($submit, CURLOPT_FRESH_CONNECT, 1);
144
145 // Apply the XML to our curl call
146 curl_setopt($submit, CURLOPT_POST, 1);
147 curl_setopt($submit, CURLOPT_POSTFIELDS, $request_xml);
148
149 $response_xml = curl_exec($submit);
150
151 if (!$response_xml) {
9248c002 152 throw new PaymentProcessorException(curl_error($submit), curl_errno($submit));
6a488035
TO
153 }
154
155 curl_close($submit);
156
b44e3f84 157 // Tidy up the response xml
6a488035
TO
158 $response_xml = preg_replace("/[\s\t]/", " ", $response_xml);
159 $response_xml = preg_replace("/[\n\r]/", "", $response_xml);
160
161 // Parse the response xml
162 $xml_parser = xml_parser_create();
163 if (!xml_parse($xml_parser, $response_xml)) {
9248c002 164 throw new PaymentProcessorException('XML Error', 9003);
6a488035
TO
165 }
166
167 $response = $this->xml_parse_into_assoc($response_xml);
168 $response = $response['#return']['RESPONSE'];
169
170 // Log the Realex response for debugging
171 // CRM_Core_Error::debug_var('REALEX --------- Response from Realex: ', $response, TRUE);
172
173 // Return an error if authentication was not successful
174 if ($response['RESULT'] !== self::AUTH_APPROVED) {
9248c002 175 throw new PaymentProcessorException($this->getDisplayError($response['RESULT'], ' ' . $response['MESSAGE']));
6a488035
TO
176 }
177
178 // FIXME: We are using the trxn_result_code column to store all these extra details since there
179 // seems to be nowhere else to put them. This is THE WRONG THING TO DO!
be2fb01f 180 $extras = [
6a488035
TO
181 'authcode' => $response['AUTHCODE'],
182 'batch_id' => $response['BATCHID'],
183 'message' => $response['MESSAGE'],
184 'trxn_result_code' => $response['RESULT'],
be2fb01f 185 ];
6a488035 186
6a488035
TO
187 $params['trxn_result_code'] = serialize($extras);
188 $params['currencyID'] = $this->_getParam('currency');
8e819886
MW
189 $result['trxn_id'] = $response['PASREF'];
190 $result['fee_amount'] = 0;
191 $result = $this->setStatusPaymentCompleted($result);
6a488035 192
8e819886 193 return $result;
6a488035
TO
194 }
195
196 /**
197 * Helper function to convert XML string to multi-dimension array.
198 *
5a4f6742 199 * @param string $xml
6a488035
TO
200 * an XML string.
201 *
a6c01b45
CW
202 * @return array
203 * An array of the result with following keys:
6a488035 204 */
00be9182 205 public function xml_parse_into_assoc($xml) {
be2fb01f
CW
206 $input = [];
207 $result = [];
6a488035
TO
208
209 $result['#error'] = FALSE;
210 $result['#return'] = NULL;
211
212 $xmlparser = xml_parser_create();
213 $ret = xml_parse_into_struct($xmlparser, $xml, $input);
214
215 xml_parser_free($xmlparser);
216
217 if (empty($input)) {
218 $result['#return'] = $xml;
219 }
220 else {
221 if ($ret > 0) {
222 $result['#return'] = $this->_xml_parse($input);
223 }
224 else {
225 $result['#error'] = ts('Error parsing XML result - error code = %1 at line %2 char %3',
be2fb01f 226 [
6a488035
TO
227 1 => xml_get_error_code($xmlparser),
228 2 => xml_get_current_line_number($xmlparser),
21dfd5f5 229 3 => xml_get_current_column_number($xmlparser),
be2fb01f 230 ]
6a488035
TO
231 );
232 }
233 }
234 return $result;
235 }
236
6c786a9b 237 /**
9248c002 238 * Private helper for xml_parse_into_assoc, to recursively parsing the result.
239 *
6c786a9b
EM
240 * @param $input
241 * @param int $depth
242 *
243 * @return array
244 */
00be9182 245 public function _xml_parse($input, $depth = 1) {
be2fb01f
CW
246 $output = [];
247 $children = [];
6a488035
TO
248
249 foreach ($input as $data) {
250 if ($data['level'] == $depth) {
251 switch ($data['type']) {
252 case 'complete':
2e1f50d6 253 $output[$data['tag']] = $data['value'] ?? '';
6a488035
TO
254 break;
255
256 case 'open':
be2fb01f 257 $children = [];
6a488035
TO
258 break;
259
260 case 'close':
261 $output[$data['tag']] = $this->_xml_parse($children, $depth + 1);
262 break;
263 }
264 }
265 else {
266 $children[] = $data;
267 }
268 }
269 return $output;
270 }
271
272 /**
ea3ddccf 273 * Format the params from the form ready for sending to Realex.
274 *
275 * Also perform some validation
276 *
277 * @param array $params
278 *
9248c002 279 * @throws \Civi\Payment\Exception\PaymentProcessorException
6a488035 280 */
00be9182 281 public function setRealexFields(&$params) {
2aa397bc 282 if ((int) $params['amount'] <= 0) {
9248c002 283 throw new PaymentProcessorException(ts('Amount must be positive'), 9001);
6a488035
TO
284 }
285
286 // format amount to be in smallest possible units
287 //list($bills, $pennies) = explode('.', $params['amount']);
288 $this->_setParam('amount', 100 * $params['amount']);
289
290 switch (strtolower($params['credit_card_type'])) {
291 case 'mastercard':
292 $this->_setParam('card_type', 'MC');
293 $this->_setParam('requiresIssueNumber', FALSE);
294 break;
295
296 case 'visa':
297 $this->_setParam('card_type', 'VISA');
298 $this->_setParam('requiresIssueNumber', FALSE);
299 break;
300
301 case 'amex':
302 $this->_setParam('card_type', 'AMEX');
303 $this->_setParam('requiresIssueNumber', FALSE);
304 break;
305
306 case 'laser':
307 $this->_setParam('card_type', 'LASER');
308 $this->_setParam('requiresIssueNumber', FALSE);
309 break;
310
311 case 'maestro':
312 case 'switch':
313 case 'maestro/switch':
314 case 'solo':
315 $this->_setParam('card_type', 'SWITCH');
316 $this->_setParam('requiresIssueNumber', TRUE);
317 break;
318
319 default:
9248c002 320 throw new PaymentProcessorException(ts('Credit card type not supported by Realex:') . ' ' . $params['credit_card_type'], 9001);
6a488035
TO
321 }
322
323 // get the card holder name - cater cor customized billing forms
324 if (isset($params['cardholder_name'])) {
325 $credit_card_name = $params['cardholder_name'];
326 }
327 else {
328 $credit_card_name = $params['first_name'] . " ";
329 if (!empty($params['middle_name'])) {
330 $credit_card_name .= $params['middle_name'] . " ";
331 }
332 $credit_card_name .= $params['last_name'];
333 }
334
335 $this->_setParam('card_name', $credit_card_name);
336 $this->_setParam('card_number', str_replace(' ', '', $params['credit_card_number']));
337 $this->_setParam('cvn', $params['cvv2']);
338 $this->_setParam('country', $params['country']);
339 $this->_setParam('post_code', $params['postal_code']);
340 $this->_setParam('order_id', $params['invoiceID']);
2e1f50d6 341 $params['issue_number'] = ($params['issue_number'] ?? '');
6a488035
TO
342 $this->_setParam('issue_number', $params['issue_number']);
343 $this->_setParam('varref', $params['contributionType_name']);
344 $comment = $params['description'] . ' (page id:' . $params['contributionPageID'] . ')';
345 $this->_setParam('comments', $comment);
6a488035
TO
346
347 // set the currency to the default which can be overrided.
9248c002 348 $this->_setParam('currency', CRM_Core_Config::singleton()->defaultCurrency);
6a488035
TO
349
350 // Format the expiry date to MMYY
2aa397bc 351 $expmonth = (string) $params['month'];
6a488035 352 $expmonth = (strlen($expmonth) === 1) ? '0' . $expmonth : $expmonth;
353ffa53 353 $expyear = substr((string) $params['year'], 2, 2);
6a488035
TO
354 $this->_setParam('exp_date', $expmonth . $expyear);
355
356 if (isset($params['credit_card_start_date']) && (strlen($params['credit_card_start_date']['M']) !== 0) &&
357 (strlen($params['credit_card_start_date']['Y']) !== 0)
358 ) {
2aa397bc 359 $startmonth = (string) $params['credit_card_start_date']['M'];
6a488035 360 $startmonth = (strlen($startmonth) === 1) ? '0' . $startmonth : $startmonth;
353ffa53 361 $startyear = substr((string) $params['credit_card_start_date']['Y'], 2, 2);
6a488035
TO
362 $this->_setParam('start_date', $startmonth . $startyear);
363 }
364
365 // Create timestamp
50b5edc4 366 $timestamp = date('YmdHis');
6a488035 367 $this->_setParam('timestamp', $timestamp);
6a488035
TO
368 }
369
6a488035 370 /**
fe482240 371 * Get the value of a field if set.
6a488035 372 *
6a0b768e
TO
373 * @param string $field
374 * The field.
6a488035 375 *
72b3a70c
CW
376 * @return mixed
377 * value of the field, or empty string if the field is
16b10e64 378 * not set
6a488035 379 */
00be9182 380 public function _getParam($field) {
6a488035
TO
381 if (isset($this->_params[$field])) {
382 return $this->_params[$field];
383 }
9248c002 384 return '';
6a488035
TO
385 }
386
387 /**
388 * Set a field to the specified value. Value must be a scalar (int,
389 * float, string, or boolean)
390 *
391 * @param string $field
392 * @param mixed $value
6a488035 393 */
00be9182 394 public function _setParam($field, $value) {
6a488035 395 if (!is_scalar($value)) {
9248c002 396 return;
6a488035 397 }
9248c002 398 $this->_params[$field] = $value;
6a488035
TO
399 }
400
401 /**
fe482240 402 * This function checks to see if we have the right config values.
6a488035 403 *
a6c01b45
CW
404 * @return string
405 * the error message if any
6a488035 406 */
00be9182 407 public function checkConfig() {
be2fb01f 408 $error = [];
6a488035
TO
409 if (empty($this->_paymentProcessor['user_name'])) {
410 $error[] = ts('Merchant ID is not set for this payment processor');
411 }
412
413 if (empty($this->_paymentProcessor['password'])) {
414 $error[] = ts('Secret is not set for this payment processor');
415 }
416
417 if (!empty($error)) {
418 return implode('<p>', $error);
419 }
9248c002 420 return NULL;
421 }
422
423 /**
424 * Get the error to display.
425 *
426 * @param string $errorCode
427 * @param string $errorMessage
428 *
429 * @return string
430 */
431 protected function getDisplayError($errorCode, $errorMessage): string {
432 if ($errorCode === '101' || $errorCode === '102') {
433 $display_error = ts('Card declined by bank. Please try with a different card.');
434 }
435 elseif ($errorCode === '103') {
436 $display_error = ts('Card reported lost or stolen. This incident will be reported.');
437 }
438 elseif ($errorCode === '501') {
439 $display_error = ts('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 for this transaction. 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.');
440 }
441 elseif ($errorCode === '509') {
442 $display_error = $errorMessage;
443 }
6a488035 444 else {
9248c002 445 $display_error = ts('We were unable to process your payment at this time. Please try again later.');
6a488035 446 }
9248c002 447 return $display_error;
6a488035 448 }
96025800 449
6a488035 450}