3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.5 |
5 +--------------------------------------------------------------------+
6 | This file is a part of CiviCRM. |
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. |
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. |
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 +--------------------------------------------------------------------+
29 * Licensed to CiviCRM under the Academic Free License version 3.0.
31 * Written and contributed by Kirkdesigns (http://www.kirkdesigns.co.uk)
38 * @author Tom Kirkpatrick <tkp@kirkdesigns.co.uk>
41 class CRM_Core_Payment_Realex
extends CRM_Core_Payment
{
42 CONST AUTH_APPROVED
= '00';
44 protected $_mode = NULL;
46 protected $_params = array();
49 * We only need one instance of this object. So we use the singleton
50 * pattern and cache the instance in this variable
55 static private $_singleton = NULL;
60 * @param string $mode the mode of operation: live or test
62 * @param $paymentProcessor
64 * @return \CRM_Core_Payment_Realex
66 function __construct($mode, &$paymentProcessor) {
68 $this->_paymentProcessor
= $paymentProcessor;
69 $this->_processorName
= ts('Realex');
71 $this->_setParam('merchant_ref', $paymentProcessor['user_name']);
72 $this->_setParam('secret', $paymentProcessor['password']);
73 $this->_setParam('account', $paymentProcessor['subject']);
75 $this->_setParam('emailCustomer', 'TRUE');
77 $this->_setParam('sequence', rand(1, 1000));
81 * singleton function used to manage this object
83 * @param string $mode the mode of operation: live or test
85 * @param object $paymentProcessor
90 static function &singleton($mode, &$paymentProcessor, &$paymentForm = NULL, $force = false) {
91 $processorName = $paymentProcessor['name'];
92 if (self
::$_singleton[$processorName] === NULL) {
93 self
::$_singleton[$processorName] = new CRM_Core_Payment_Realex($mode, $paymentProcessor);
95 return self
::$_singleton[$processorName];
103 function setExpressCheckOut(&$params) {
104 CRM_Core_Error
::fatal(ts('This function is not implemented'));
112 function getExpressCheckoutDetails($token) {
113 CRM_Core_Error
::fatal(ts('This function is not implemented'));
121 function doExpressCheckout(&$params) {
122 CRM_Core_Error
::fatal(ts('This function is not implemented'));
130 function doTransferCheckout(&$params) {
131 CRM_Core_Error
::fatal(ts('This function is not implemented'));
135 * Submit a payment using Advanced Integration Method
137 * @param array $params assoc array of input parameters for this transaction
139 * @return array the result in a nice formatted array (or an error object)
142 function doDirectPayment(&$params) {
144 if (!defined('CURLOPT_SSLCERT')) {
145 return self
::error(9001, ts('RealAuth requires curl with SSL support'));
148 $result = $this->setRealexFields($params);
150 if ($result !== TRUE) {
154 /**********************************************************
155 * Check to see if we have a duplicate before we send
156 **********************************************************/
157 if ($this->_checkDupe($this->_getParam('order_id'))) {
158 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.'));
161 // Create sha1 hash for request
162 $hashme = "{$this->_getParam('timestamp')}.{$this->_getParam('merchant_ref')}.{$this->_getParam('order_id')}.{$this->_getParam('amount')}.{$this->_getParam('currency')}.{$this->_getParam('card_number')}";
163 $sha1hash = sha1($hashme);
164 $hashme = "$sha1hash.{$this->_getParam('secret')}";
165 $sha1hash = sha1($hashme);
168 // Generate the request xml that is send to Realex Payments.
169 $request_xml = "<request type='auth' timestamp='{$this->_getParam('timestamp')}'>
170 <merchantid>{$this->_getParam('merchant_ref')}</merchantid>
171 <account>{$this->_getParam('account')}</account>
172 <orderid>{$this->_getParam('order_id')}</orderid>
173 <amount currency='{$this->_getParam('currency')}'>{$this->_getParam('amount')}</amount>
175 <number>{$this->_getParam('card_number')}</number>
176 <expdate>{$this->_getParam('exp_date')}</expdate>
177 <type>{$this->_getParam('card_type')}</type>
178 <chname>{$this->_getParam('card_name')}</chname>
179 <issueno>{$this->_getParam('issue_number')}</issueno>
181 <number>{$this->_getParam('cvn')}</number>
185 <autosettle flag='1'/>
186 <sha1hash>$sha1hash</sha1hash>
188 <comment id='1'>{$this->_getParam('comments')}</comment>
191 <varref>{$this->_getParam('varref')}</varref>
195 /**********************************************************
196 * Send to the payment processor using cURL
197 **********************************************************/
199 $submit = curl_init($this->_paymentProcessor
['url_site']);
202 return self
::error(9002, ts('Could not initiate connection to payment gateway'));
205 curl_setopt($submit, CURLOPT_HTTPHEADER
, array('SOAPAction: ""'));
206 curl_setopt($submit, CURLOPT_RETURNTRANSFER
, 1);
207 curl_setopt($submit, CURLOPT_TIMEOUT
, 60);
208 curl_setopt($submit, CURLOPT_SSL_VERIFYPEER
, CRM_Core_BAO_Setting
::getItem(CRM_Core_BAO_Setting
::SYSTEM_PREFERENCES_NAME
, 'verifySSL'));
209 curl_setopt($submit, CURLOPT_HEADER
, 0);
211 // take caching out of the picture
212 curl_setopt($submit, CURLOPT_FORBID_REUSE
, 1);
213 curl_setopt($submit, CURLOPT_FRESH_CONNECT
, 1);
215 // Apply the XML to our curl call
216 curl_setopt($submit, CURLOPT_POST
, 1);
217 curl_setopt($submit, CURLOPT_POSTFIELDS
, $request_xml);
219 $response_xml = curl_exec($submit);
221 if (!$response_xml) {
222 return self
::error(curl_errno($submit), curl_error($submit));
227 // Tidy up the responce xml
228 $response_xml = preg_replace("/[\s\t]/", " ", $response_xml);
229 $response_xml = preg_replace("/[\n\r]/", "", $response_xml);
231 // Parse the response xml
232 $xml_parser = xml_parser_create();
233 if (!xml_parse($xml_parser, $response_xml)) {
234 return self
::error(9003, 'XML Error');
237 $response = $this->xml_parse_into_assoc($response_xml);
238 $response = $response['#return']['RESPONSE'];
240 // Log the Realex response for debugging
241 // CRM_Core_Error::debug_var('REALEX --------- Response from Realex: ', $response, TRUE);
243 // Return an error if authentication was not successful
244 if ($response['RESULT'] !== self
::AUTH_APPROVED
) {
245 return self
::error($response['RESULT'], ' ' . $response['MESSAGE']);
248 // Check the response hash
249 $hashme = "{$this->_getParam('timestamp')}.{$this->_getParam('merchant_ref')}.{$this->_getParam('order_id')}.{$response['RESULT']}.{$response['MESSAGE']}.{$response['PASREF']}.{$response['AUTHCODE']}";
250 $sha1hash = sha1($hashme);
251 $hashme = "$sha1hash.{$this->_getParam('secret')}";
252 $sha1hash = sha1($hashme);
254 if ($response['SHA1HASH'] != $sha1hash) {
255 // FIXME: Need to actually check this - I couldn't get the
256 // hashes to match so I'm commenting out for now'
257 // return self::error( 9001, "Hash error, please report this to the webmaster" );
260 // FIXME: We are using the trxn_result_code column to store all these extra details since there
261 // seems to be nowhere else to put them. This is THE WRONG THING TO DO!
263 'authcode' => $response['AUTHCODE'],
264 'batch_id' => $response['BATCHID'],
265 'message' => $response['MESSAGE'],
266 'trxn_result_code' => $response['RESULT'],
269 $params['trxn_id'] = $response['PASREF'];
270 $params['trxn_result_code'] = serialize($extras);
271 $params['currencyID'] = $this->_getParam('currency');
272 $params['gross_amount'] = $this->_getParam('amount');
273 $params['fee_amount'] = 0;
279 * Helper function to convert XML string to multi-dimension array.
284 * @return array An array of the result with following keys:
286 function xml_parse_into_assoc($xml) {
290 $result['#error'] = FALSE;
291 $result['#return'] = NULL;
293 $xmlparser = xml_parser_create();
294 $ret = xml_parse_into_struct($xmlparser, $xml, $input);
296 xml_parser_free($xmlparser);
299 $result['#return'] = $xml;
303 $result['#return'] = $this->_xml_parse($input);
306 $result['#error'] = ts('Error parsing XML result - error code = %1 at line %2 char %3',
308 1 => xml_get_error_code($xmlparser),
309 2 => xml_get_current_line_number($xmlparser),
310 3 => xml_get_current_column_number($xmlparser)
318 // private helper for xml_parse_into_assoc, to recusively parsing the result
325 function _xml_parse($input, $depth = 1) {
329 foreach ($input as $data) {
330 if ($data['level'] == $depth) {
331 switch ($data['type']) {
333 $output[$data['tag']] = isset($data['value']) ?
$data['value'] : '';
341 $output[$data['tag']] = $this->_xml_parse($children, $depth +
1);
353 * Format the params from the form ready for sending to Realex. Also perform some validation
355 function setRealexFields(&$params) {
356 if ((int)$params['amount'] <= 0) {
357 return self
::error(9001, ts('Amount must be positive'));
360 // format amount to be in smallest possible units
361 //list($bills, $pennies) = explode('.', $params['amount']);
362 $this->_setParam('amount', 100 * $params['amount']);
364 switch (strtolower($params['credit_card_type'])) {
366 $this->_setParam('card_type', 'MC');
367 $this->_setParam('requiresIssueNumber', FALSE);
371 $this->_setParam('card_type', 'VISA');
372 $this->_setParam('requiresIssueNumber', FALSE);
376 $this->_setParam('card_type', 'AMEX');
377 $this->_setParam('requiresIssueNumber', FALSE);
381 $this->_setParam('card_type', 'LASER');
382 $this->_setParam('requiresIssueNumber', FALSE);
387 case 'maestro/switch':
389 $this->_setParam('card_type', 'SWITCH');
390 $this->_setParam('requiresIssueNumber', TRUE);
394 return self
::error(9001, ts('Credit card type not supported by Realex:') . ' ' . $params['credit_card_type']);
397 // get the card holder name - cater cor customized billing forms
398 if (isset($params['cardholder_name'])) {
399 $credit_card_name = $params['cardholder_name'];
402 $credit_card_name = $params['first_name'] . " ";
403 if (!empty($params['middle_name'])) {
404 $credit_card_name .= $params['middle_name'] . " ";
406 $credit_card_name .= $params['last_name'];
409 $this->_setParam('card_name', $credit_card_name);
410 $this->_setParam('card_number', str_replace(' ', '', $params['credit_card_number']));
411 $this->_setParam('cvn', $params['cvv2']);
412 $this->_setParam('country', $params['country']);
413 $this->_setParam('post_code', $params['postal_code']);
414 $this->_setParam('order_id', $params['invoiceID']);
415 $params['issue_number'] = (isset($params['issue_number']) ?
$params['issue_number'] : '');
416 $this->_setParam('issue_number', $params['issue_number']);
417 $this->_setParam('varref', $params['contributionType_name']);
418 $comment = $params['description'] . ' (page id:' . $params['contributionPageID'] . ')';
419 $this->_setParam('comments', $comment);
420 //$this->_setParam('currency', $params['currencyID']);
422 // set the currency to the default which can be overrided.
423 $config = CRM_Core_Config
::singleton();
424 $this->_setParam('currency', $config->defaultCurrency
);
426 // Format the expiry date to MMYY
427 $expmonth = (string)$params['month'];
428 $expmonth = (strlen($expmonth) === 1) ?
'0' . $expmonth : $expmonth;
429 $expyear = substr((string)$params['year'], 2, 2);
430 $this->_setParam('exp_date', $expmonth . $expyear);
432 if (isset($params['credit_card_start_date']) && (strlen($params['credit_card_start_date']['M']) !== 0) &&
433 (strlen($params['credit_card_start_date']['Y']) !== 0)
435 $startmonth = (string)$params['credit_card_start_date']['M'];
436 $startmonth = (strlen($startmonth) === 1) ?
'0' . $startmonth : $startmonth;
437 $startyear = substr((string)$params['credit_card_start_date']['Y'], 2, 2);
438 $this->_setParam('start_date', $startmonth . $startyear);
442 $timestamp = strftime("%Y%m%d%H%M%S");
443 $this->_setParam('timestamp', $timestamp);
449 * Checks to see if invoice_id already exists in db
451 * @param int $invoiceId The ID to check
453 * @return bool True if ID exists, else false
455 function _checkDupe($invoiceId) {
456 $contribution = new CRM_Contribute_DAO_Contribution();
457 $contribution->invoice_id
= $invoiceId;
458 return $contribution->find();
462 * Get the value of a field if set
464 * @param string $field the field
466 * @return mixed value of the field, or empty string if the field is
469 function _getParam($field) {
470 if (isset($this->_params
[$field])) {
471 return $this->_params
[$field];
479 * Set a field to the specified value. Value must be a scalar (int,
480 * float, string, or boolean)
482 * @param string $field
483 * @param mixed $value
485 * @return bool false if value is not a scalar, true if successful
487 function _setParam($field, $value) {
488 if (!is_scalar($value)) {
492 $this->_params
[$field] = $value;
497 * @param null $errorCode
498 * @param null $errorMessage
502 function &error($errorCode = NULL, $errorMessage = NULL) {
503 $e = CRM_Core_Error
::singleton();
506 if ($errorCode == '101' ||
$errorCode == '102') {
507 $display_error = ts('Card declined by bank. Please try with a different card.');
509 elseif ($errorCode == '103') {
510 $display_error = ts('Card reported lost or stolen. This incident will be reported.');
512 elseif ($errorCode == '501') {
513 $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.");
515 elseif ($errorCode == '509') {
516 $display_error = $errorMessage;
519 $display_error = ts('We were unable to process your payment at this time. Please try again later.');
521 $e->push($errorCode, 0, NULL, $display_error);
524 $e->push(9001, 0, NULL, ts('We were unable to process your payment at this time. Please try again later.'));
530 * This function checks to see if we have the right config values
532 * @return string the error message if any
535 function checkConfig() {
537 if (empty($this->_paymentProcessor
['user_name'])) {
538 $error[] = ts('Merchant ID is not set for this payment processor');
541 if (empty($this->_paymentProcessor
['password'])) {
542 $error[] = ts('Secret is not set for this payment processor');
545 if (!empty($error)) {
546 return implode('<p>', $error);