Merge pull request #4898 from monishdeb/CRM-15619-fix
[civicrm-core.git] / CRM / Core / Payment / Realex.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
39de6fd5 4 | CiviCRM version 4.6 |
6a488035
TO
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/*
28 * Copyright (C) 2009
29 * Licensed to CiviCRM under the Academic Free License version 3.0.
30 *
31 * Written and contributed by Kirkdesigns (http://www.kirkdesigns.co.uk)
6a488035
TO
32 */
33
34/**
35 *
36 * @package CRM
37 * @author Tom Kirkpatrick <tkp@kirkdesigns.co.uk>
38 * $Id$
39 */
40class CRM_Core_Payment_Realex extends CRM_Core_Payment {
7da04cde 41 const AUTH_APPROVED = '00';
6a488035
TO
42
43 protected $_mode = NULL;
44
45 protected $_params = array();
46
47 /**
48 * We only need one instance of this object. So we use the singleton
49 * pattern and cache the instance in this variable
50 *
51 * @var object
52 * @static
53 */
54 static private $_singleton = NULL;
55
56 /**
57 * Constructor
58 *
6a0b768e
TO
59 * @param string $mode
60 * The mode of operation: live or test.
6a488035 61 *
77b97be7
EM
62 * @param $paymentProcessor
63 *
64 * @return \CRM_Core_Payment_Realex
6a488035 65 */
00be9182 66 public function __construct($mode, &$paymentProcessor) {
6a488035
TO
67 $this->_mode = $mode;
68 $this->_paymentProcessor = $paymentProcessor;
69 $this->_processorName = ts('Realex');
70
71 $this->_setParam('merchant_ref', $paymentProcessor['user_name']);
72 $this->_setParam('secret', $paymentProcessor['password']);
73 $this->_setParam('account', $paymentProcessor['subject']);
74
75 $this->_setParam('emailCustomer', 'TRUE');
76 srand(time());
77 $this->_setParam('sequence', rand(1, 1000));
78 }
79
6c786a9b 80 /**
c490a46a 81 * @param array $params
6c786a9b
EM
82 *
83 * @throws Exception
84 */
00be9182 85 public function setExpressCheckOut(&$params) {
6a488035
TO
86 CRM_Core_Error::fatal(ts('This function is not implemented'));
87 }
88
6c786a9b
EM
89 /**
90 * @param $token
91 *
92 * @throws Exception
93 */
00be9182 94 public function getExpressCheckoutDetails($token) {
6a488035
TO
95 CRM_Core_Error::fatal(ts('This function is not implemented'));
96 }
97
6c786a9b 98 /**
c490a46a 99 * @param array $params
6c786a9b
EM
100 *
101 * @throws Exception
102 */
00be9182 103 public function doExpressCheckout(&$params) {
6a488035
TO
104 CRM_Core_Error::fatal(ts('This function is not implemented'));
105 }
106
6c786a9b 107 /**
c490a46a 108 * @param array $params
6c786a9b
EM
109 *
110 * @throws Exception
111 */
00be9182 112 public function doTransferCheckout(&$params) {
6a488035
TO
113 CRM_Core_Error::fatal(ts('This function is not implemented'));
114 }
115
116 /**
117 * Submit a payment using Advanced Integration Method
118 *
6a0b768e
TO
119 * @param array $params
120 * Assoc array of input parameters for this transaction.
6a488035 121 *
a6c01b45
CW
122 * @return array
123 * the result in a nice formatted array (or an error object)
6a488035 124 */
00be9182 125 public function doDirectPayment(&$params) {
6a488035
TO
126
127 if (!defined('CURLOPT_SSLCERT')) {
128 return self::error(9001, ts('RealAuth requires curl with SSL support'));
129 }
130
131 $result = $this->setRealexFields($params);
132
133 if ($result !== TRUE) {
134 return $result;
135 }
136
137 /**********************************************************
138 * Check to see if we have a duplicate before we send
139 **********************************************************/
140 if ($this->_checkDupe($this->_getParam('order_id'))) {
141 return self::error(9004, 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.'));
142 }
143
144 // Create sha1 hash for request
353ffa53 145 $hashme = "{$this->_getParam('timestamp')}.{$this->_getParam('merchant_ref')}.{$this->_getParam('order_id')}.{$this->_getParam('amount')}.{$this->_getParam('currency')}.{$this->_getParam('card_number')}";
6a488035 146 $sha1hash = sha1($hashme);
353ffa53 147 $hashme = "$sha1hash.{$this->_getParam('secret')}";
6a488035
TO
148 $sha1hash = sha1($hashme);
149
6a488035
TO
150 // Generate the request xml that is send to Realex Payments.
151 $request_xml = "<request type='auth' timestamp='{$this->_getParam('timestamp')}'>
152 <merchantid>{$this->_getParam('merchant_ref')}</merchantid>
153 <account>{$this->_getParam('account')}</account>
154 <orderid>{$this->_getParam('order_id')}</orderid>
155 <amount currency='{$this->_getParam('currency')}'>{$this->_getParam('amount')}</amount>
156 <card>
157 <number>{$this->_getParam('card_number')}</number>
158 <expdate>{$this->_getParam('exp_date')}</expdate>
159 <type>{$this->_getParam('card_type')}</type>
160 <chname>{$this->_getParam('card_name')}</chname>
161 <issueno>{$this->_getParam('issue_number')}</issueno>
162 <cvn>
163 <number>{$this->_getParam('cvn')}</number>
164 <presind>1</presind>
165 </cvn>
166 </card>
167 <autosettle flag='1'/>
168 <sha1hash>$sha1hash</sha1hash>
169 <comments>
170 <comment id='1'>{$this->_getParam('comments')}</comment>
171 </comments>
172 <tssinfo>
173 <varref>{$this->_getParam('varref')}</varref>
174 </tssinfo>
175 </request>";
176
177 /**********************************************************
178 * Send to the payment processor using cURL
179 **********************************************************/
180
181 $submit = curl_init($this->_paymentProcessor['url_site']);
182
183 if (!$submit) {
184 return self::error(9002, ts('Could not initiate connection to payment gateway'));
185 }
186
187 curl_setopt($submit, CURLOPT_HTTPHEADER, array('SOAPAction: ""'));
188 curl_setopt($submit, CURLOPT_RETURNTRANSFER, 1);
189 curl_setopt($submit, CURLOPT_TIMEOUT, 60);
190 curl_setopt($submit, CURLOPT_SSL_VERIFYPEER, CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'verifySSL'));
191 curl_setopt($submit, CURLOPT_HEADER, 0);
192
193 // take caching out of the picture
194 curl_setopt($submit, CURLOPT_FORBID_REUSE, 1);
195 curl_setopt($submit, CURLOPT_FRESH_CONNECT, 1);
196
197 // Apply the XML to our curl call
198 curl_setopt($submit, CURLOPT_POST, 1);
199 curl_setopt($submit, CURLOPT_POSTFIELDS, $request_xml);
200
201 $response_xml = curl_exec($submit);
202
203 if (!$response_xml) {
204 return self::error(curl_errno($submit), curl_error($submit));
205 }
206
207 curl_close($submit);
208
209 // Tidy up the responce xml
210 $response_xml = preg_replace("/[\s\t]/", " ", $response_xml);
211 $response_xml = preg_replace("/[\n\r]/", "", $response_xml);
212
213 // Parse the response xml
214 $xml_parser = xml_parser_create();
215 if (!xml_parse($xml_parser, $response_xml)) {
216 return self::error(9003, 'XML Error');
217 }
218
219 $response = $this->xml_parse_into_assoc($response_xml);
220 $response = $response['#return']['RESPONSE'];
221
222 // Log the Realex response for debugging
223 // CRM_Core_Error::debug_var('REALEX --------- Response from Realex: ', $response, TRUE);
224
225 // Return an error if authentication was not successful
226 if ($response['RESULT'] !== self::AUTH_APPROVED) {
227 return self::error($response['RESULT'], ' ' . $response['MESSAGE']);
228 }
229
230 // Check the response hash
353ffa53 231 $hashme = "{$this->_getParam('timestamp')}.{$this->_getParam('merchant_ref')}.{$this->_getParam('order_id')}.{$response['RESULT']}.{$response['MESSAGE']}.{$response['PASREF']}.{$response['AUTHCODE']}";
6a488035 232 $sha1hash = sha1($hashme);
353ffa53 233 $hashme = "$sha1hash.{$this->_getParam('secret')}";
6a488035
TO
234 $sha1hash = sha1($hashme);
235
236 if ($response['SHA1HASH'] != $sha1hash) {
237 // FIXME: Need to actually check this - I couldn't get the
238 // hashes to match so I'm commenting out for now'
239 // return self::error( 9001, "Hash error, please report this to the webmaster" );
240 }
241
242 // FIXME: We are using the trxn_result_code column to store all these extra details since there
243 // seems to be nowhere else to put them. This is THE WRONG THING TO DO!
244 $extras = array(
245 'authcode' => $response['AUTHCODE'],
246 'batch_id' => $response['BATCHID'],
247 'message' => $response['MESSAGE'],
248 'trxn_result_code' => $response['RESULT'],
249 );
250
251 $params['trxn_id'] = $response['PASREF'];
252 $params['trxn_result_code'] = serialize($extras);
253 $params['currencyID'] = $this->_getParam('currency');
254 $params['gross_amount'] = $this->_getParam('amount');
255 $params['fee_amount'] = 0;
256
257 return $params;
258 }
259
260 /**
261 * Helper function to convert XML string to multi-dimension array.
262 *
5a4f6742 263 * @param string $xml
6a488035
TO
264 * an XML string.
265 *
a6c01b45
CW
266 * @return array
267 * An array of the result with following keys:
6a488035 268 */
00be9182 269 public function xml_parse_into_assoc($xml) {
6a488035
TO
270 $input = array();
271 $result = array();
272
273 $result['#error'] = FALSE;
274 $result['#return'] = NULL;
275
276 $xmlparser = xml_parser_create();
277 $ret = xml_parse_into_struct($xmlparser, $xml, $input);
278
279 xml_parser_free($xmlparser);
280
281 if (empty($input)) {
282 $result['#return'] = $xml;
283 }
284 else {
285 if ($ret > 0) {
286 $result['#return'] = $this->_xml_parse($input);
287 }
288 else {
289 $result['#error'] = ts('Error parsing XML result - error code = %1 at line %2 char %3',
290 array(
291 1 => xml_get_error_code($xmlparser),
292 2 => xml_get_current_line_number($xmlparser),
21dfd5f5 293 3 => xml_get_current_column_number($xmlparser),
6a488035
TO
294 )
295 );
296 }
297 }
298 return $result;
299 }
300
6c786a9b 301 /**
5a4f6742 302 * Private helper for xml_parse_into_assoc, to recusively parsing the result
6c786a9b
EM
303 * @param $input
304 * @param int $depth
305 *
306 * @return array
307 */
00be9182 308 public function _xml_parse($input, $depth = 1) {
6a488035
TO
309 $output = array();
310 $children = array();
311
312 foreach ($input as $data) {
313 if ($data['level'] == $depth) {
314 switch ($data['type']) {
315 case 'complete':
316 $output[$data['tag']] = isset($data['value']) ? $data['value'] : '';
317 break;
318
319 case 'open':
320 $children = array();
321 break;
322
323 case 'close':
324 $output[$data['tag']] = $this->_xml_parse($children, $depth + 1);
325 break;
326 }
327 }
328 else {
329 $children[] = $data;
330 }
331 }
332 return $output;
333 }
334
335 /**
336 * Format the params from the form ready for sending to Realex. Also perform some validation
337 */
00be9182 338 public function setRealexFields(&$params) {
2aa397bc 339 if ((int) $params['amount'] <= 0) {
6a488035
TO
340 return self::error(9001, ts('Amount must be positive'));
341 }
342
343 // format amount to be in smallest possible units
344 //list($bills, $pennies) = explode('.', $params['amount']);
345 $this->_setParam('amount', 100 * $params['amount']);
346
347 switch (strtolower($params['credit_card_type'])) {
348 case 'mastercard':
349 $this->_setParam('card_type', 'MC');
350 $this->_setParam('requiresIssueNumber', FALSE);
351 break;
352
353 case 'visa':
354 $this->_setParam('card_type', 'VISA');
355 $this->_setParam('requiresIssueNumber', FALSE);
356 break;
357
358 case 'amex':
359 $this->_setParam('card_type', 'AMEX');
360 $this->_setParam('requiresIssueNumber', FALSE);
361 break;
362
363 case 'laser':
364 $this->_setParam('card_type', 'LASER');
365 $this->_setParam('requiresIssueNumber', FALSE);
366 break;
367
368 case 'maestro':
369 case 'switch':
370 case 'maestro/switch':
371 case 'solo':
372 $this->_setParam('card_type', 'SWITCH');
373 $this->_setParam('requiresIssueNumber', TRUE);
374 break;
375
376 default:
377 return self::error(9001, ts('Credit card type not supported by Realex:') . ' ' . $params['credit_card_type']);
378 }
379
380 // get the card holder name - cater cor customized billing forms
381 if (isset($params['cardholder_name'])) {
382 $credit_card_name = $params['cardholder_name'];
383 }
384 else {
385 $credit_card_name = $params['first_name'] . " ";
386 if (!empty($params['middle_name'])) {
387 $credit_card_name .= $params['middle_name'] . " ";
388 }
389 $credit_card_name .= $params['last_name'];
390 }
391
392 $this->_setParam('card_name', $credit_card_name);
393 $this->_setParam('card_number', str_replace(' ', '', $params['credit_card_number']));
394 $this->_setParam('cvn', $params['cvv2']);
395 $this->_setParam('country', $params['country']);
396 $this->_setParam('post_code', $params['postal_code']);
397 $this->_setParam('order_id', $params['invoiceID']);
398 $params['issue_number'] = (isset($params['issue_number']) ? $params['issue_number'] : '');
399 $this->_setParam('issue_number', $params['issue_number']);
400 $this->_setParam('varref', $params['contributionType_name']);
401 $comment = $params['description'] . ' (page id:' . $params['contributionPageID'] . ')';
402 $this->_setParam('comments', $comment);
403 //$this->_setParam('currency', $params['currencyID']);
404
405 // set the currency to the default which can be overrided.
406 $config = CRM_Core_Config::singleton();
407 $this->_setParam('currency', $config->defaultCurrency);
408
409 // Format the expiry date to MMYY
2aa397bc 410 $expmonth = (string) $params['month'];
6a488035 411 $expmonth = (strlen($expmonth) === 1) ? '0' . $expmonth : $expmonth;
353ffa53 412 $expyear = substr((string) $params['year'], 2, 2);
6a488035
TO
413 $this->_setParam('exp_date', $expmonth . $expyear);
414
415 if (isset($params['credit_card_start_date']) && (strlen($params['credit_card_start_date']['M']) !== 0) &&
416 (strlen($params['credit_card_start_date']['Y']) !== 0)
417 ) {
2aa397bc 418 $startmonth = (string) $params['credit_card_start_date']['M'];
6a488035 419 $startmonth = (strlen($startmonth) === 1) ? '0' . $startmonth : $startmonth;
353ffa53 420 $startyear = substr((string) $params['credit_card_start_date']['Y'], 2, 2);
6a488035
TO
421 $this->_setParam('start_date', $startmonth . $startyear);
422 }
423
424 // Create timestamp
425 $timestamp = strftime("%Y%m%d%H%M%S");
426 $this->_setParam('timestamp', $timestamp);
427
428 return TRUE;
429 }
430
431 /**
432 * Checks to see if invoice_id already exists in db
433 *
6a0b768e
TO
434 * @param int $invoiceId
435 * The ID to check.
6a488035 436 *
a6c01b45
CW
437 * @return bool
438 * True if ID exists, else false
6a488035 439 */
00be9182 440 public function _checkDupe($invoiceId) {
6a488035
TO
441 $contribution = new CRM_Contribute_DAO_Contribution();
442 $contribution->invoice_id = $invoiceId;
443 return $contribution->find();
444 }
445
446 /**
447 * Get the value of a field if set
448 *
6a0b768e
TO
449 * @param string $field
450 * The field.
6a488035 451 *
72b3a70c
CW
452 * @return mixed
453 * value of the field, or empty string if the field is
16b10e64 454 * not set
6a488035 455 */
00be9182 456 public function _getParam($field) {
6a488035
TO
457 if (isset($this->_params[$field])) {
458 return $this->_params[$field];
459 }
460 else {
461 return '';
462 }
463 }
464
465 /**
466 * Set a field to the specified value. Value must be a scalar (int,
467 * float, string, or boolean)
468 *
469 * @param string $field
470 * @param mixed $value
471 *
a6c01b45
CW
472 * @return bool
473 * false if value is not a scalar, true if successful
6a488035 474 */
00be9182 475 public function _setParam($field, $value) {
6a488035
TO
476 if (!is_scalar($value)) {
477 return FALSE;
478 }
479 else {
480 $this->_params[$field] = $value;
481 }
482 }
483
6c786a9b
EM
484 /**
485 * @param null $errorCode
486 * @param null $errorMessage
487 *
488 * @return object
489 */
00be9182 490 public function &error($errorCode = NULL, $errorMessage = NULL) {
6a488035
TO
491 $e = CRM_Core_Error::singleton();
492
493 if ($errorCode) {
494 if ($errorCode == '101' || $errorCode == '102') {
495 $display_error = ts('Card declined by bank. Please try with a different card.');
496 }
497 elseif ($errorCode == '103') {
498 $display_error = ts('Card reported lost or stolen. This incident will be reported.');
499 }
500 elseif ($errorCode == '501') {
501 $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.");
502 }
503 elseif ($errorCode == '509') {
504 $display_error = $errorMessage;
505 }
506 else {
507 $display_error = ts('We were unable to process your payment at this time. Please try again later.');
508 }
509 $e->push($errorCode, 0, NULL, $display_error);
510 }
511 else {
512 $e->push(9001, 0, NULL, ts('We were unable to process your payment at this time. Please try again later.'));
513 }
514 return $e;
515 }
516
517 /**
518 * This function checks to see if we have the right config values
519 *
a6c01b45
CW
520 * @return string
521 * the error message if any
6a488035 522 */
00be9182 523 public function checkConfig() {
6a488035
TO
524 $error = array();
525 if (empty($this->_paymentProcessor['user_name'])) {
526 $error[] = ts('Merchant ID is not set for this payment processor');
527 }
528
529 if (empty($this->_paymentProcessor['password'])) {
530 $error[] = ts('Secret is not set for this payment processor');
531 }
532
533 if (!empty($error)) {
534 return implode('<p>', $error);
535 }
536 else {
537 return NULL;
538 }
539 }
540}