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