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