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