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