Merge pull request #4630 from atif-shaikh/CRM-15546
[civicrm-core.git] / CRM / Core / Payment / Elavon.php
1 <?php
2 /*
3 +----------------------------------------------------------------------------+
4 | Elavon (Nova) Virtual Merchant Core Payment Module for CiviCRM version 4.5 |
5 +----------------------------------------------------------------------------+
6 | Licensed to CiviCRM under the Academic Free License version 3.0 |
7 | |
8 | Written & Contributed by Eileen McNaughton - Nov March 2008 |
9 +----------------------------------------------------------------------------+
10 */
11
12 /**
13 -----------------------------------------------------------------------------------------------
14 The basic functionality of this processor is that variables from the $params object are transformed
15 into xml. The xml is submitted to the processor's https site
16 using curl and the response is translated back into an array using the processor's function.
17
18 If an array ($params) is returned to the calling function the values from
19 the array are merged into the calling functions array.
20
21 If an result of class error is returned it is treated as a failure. No error denotes a success. Be
22 WARY of this when coding
23
24 -----------------------------------------------------------------------------------------------
25 **/
26 class CRM_Core_Payment_Elavon extends CRM_Core_Payment {
27 // (not used, implicit in the API, might need to convert?)
28 CONST
29 CHARSET = 'UFT-8';
30
31 /**
32 * We only need one instance of this object. So we use the singleton
33 * pattern and cache the instance in this variable
34 *
35 * @var object
36 * @static
37 */
38 static private $_singleton = NULL;
39
40 /**********************************************************
41 * Constructor
42 *
43 * @param string $mode the mode of operation: live or test
44 *
45 * @param $paymentProcessor
46 *
47 * @return \CRM_Core_Payment_Elavon *******************************************************
48 */
49 function __construct($mode, &$paymentProcessor) {
50 // live or test
51 $this->_mode = $mode;
52 $this->_paymentProcessor = $paymentProcessor;
53 $this->_processorName = ts('Elavon');
54 }
55
56 /**
57 * Singleton function used to manage this object
58 *
59 * @param string $mode the mode of operation: live or test
60 *
61 * @param object $paymentProcessor
62 *
63 * @return object
64 * @static
65 */
66 static function &singleton($mode, &$paymentProcessor) {
67 $processorName = $paymentProcessor['name'];
68 if (self::$_singleton[$processorName] === NULL) {
69 self::$_singleton[$processorName] = new CRM_Core_Payment_Elavon($mode, $paymentProcessor);
70 }
71 return self::$_singleton[$processorName];
72 }
73
74 /**********************************************************
75 * This function is set up and put here to make the mapping of fields
76 * from the params object as visually clear as possible for easy editing
77 *
78 * Comment out irrelevant fields
79 **********************************************************/
80 function mapProcessorFieldstoParams($params) {
81
82 /**********************************************************
83 * compile array
84 * Payment Processor field name fields from $params array
85 **********************************************************/
86 // credit card name
87 $requestFields['ssl_first_name'] = $params['billing_first_name'];
88 // credit card name
89 //$requestFields['ssl_middle_name'] = $params['billing_middle_name'];
90 // credit card name
91 $requestFields['ssl_last_name'] = $params['billing_last_name'];
92 // contact name
93 $requestFields['ssl_ship_to_first_name'] = $params['first_name'];
94 // contact name
95 $requestFields['ssl_ship_to_last_name'] = $params['last_name'];
96 $requestFields['ssl_card_number'] = $params['credit_card_number'];
97 $requestFields['ssl_amount'] = trim($params['amount']);
98 $requestFields['ssl_exp_date'] = sprintf('%02d', (int) $params['month']) . substr($params['year'], 2, 2);;
99 $requestFields['ssl_cvv2cvc2'] = $params['cvv2'];
100 // CVV field passed to processor
101 $requestFields['ssl_cvv2cvc2_indicator'] = "1";
102 $requestFields['ssl_avs_address'] = $params['street_address'];
103 $requestFields['ssl_city'] = $params['city'];
104 $requestFields['ssl_state'] = $params['state_province'];
105 $requestFields['ssl_avs_zip'] = $params['postal_code'];
106 $requestFields['ssl_country'] = $params['country'];
107 $requestFields['ssl_email'] = $params['email'];
108 // 32 character string
109 $requestFields['ssl_invoice_number'] = $params['invoiceID'];
110 $requestFields['ssl_transaction_type'] = "CCSALE";
111 $requestFields['ssl_description'] = empty($params['description']) ? "backoffice payment" : $params['description'];
112 $requestFields['ssl_customer_number'] = substr($params['credit_card_number'], -4);
113 // Added two lines below to allow commercial cards to go through as per page 15 of Elavon developer guide
114 $requestFields['ssl_customer_code'] = '1111';
115 $requestFields['ssl_salestax'] = 0.0;
116
117
118 /************************************************************************************
119 * Fields available from civiCRM not implemented for Elavon
120 *
121 * $params['qfKey'];
122 * $params['amount_other'];
123 * $params['ip_address'];
124 * $params['contributionType_name' ];
125 * $params['contributionPageID'];
126 * $params['contributionType_accounting_code'];
127 * $params['amount_level'];
128 * $params['credit_card_type'];
129 ************************************************************************************/
130 return $requestFields;
131 }
132
133 /**********************************************************
134 * This function sends request and receives response from
135 * the processor
136 **********************************************************/
137 function doDirectPayment(&$params) {
138 if (isset($params['is_recur']) && $params['is_recur'] == TRUE) {
139 CRM_Core_Error::fatal(ts('Elavon - recurring payments not implemented'));
140 }
141
142 if (!defined('CURLOPT_SSLCERT')) {
143 CRM_Core_Error::fatal(ts('Elavon / Nova Virtual Merchant Gateway requires curl with SSL support'));
144 }
145
146 /*
147 *Create the array of variables to be sent to the processor from the $params array
148 * passed into this function
149 */
150
151 $requestFields = self::mapProcessorFieldstoParams($params);
152
153 /*
154 * define variables for connecting with the gateway
155 */
156
157 $requestFields['ssl_merchant_id'] = $this->_paymentProcessor['user_name'];
158 $requestFields['ssl_user_id'] = $this->_paymentProcessor['password'];
159 $requestFields['ssl_pin'] = $this->_paymentProcessor['signature'];
160 $host = $this->_paymentProcessor['url_site'];
161
162 if ($this->_mode == "test") {
163 $requestFields['ssl_test_mode'] = "TRUE";
164 }
165
166 // Allow further manipulation of the arguments via custom hooks ..
167 CRM_Utils_Hook::alterPaymentProcessorParams($this, $params, $requestFields);
168
169 /**********************************************************
170 * Check to see if we have a duplicate before we send
171 **********************************************************/
172 if ($this->_checkDupe($params['invoiceID'])) {
173 return self::errorExit(9003, '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. 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.');
174 }
175
176 /**********************************************************
177 * Convert to XML using function below
178 **********************************************************/
179 $xml = self::buildXML($requestFields);
180
181 /**********************************************************
182 * Send to the payment processor using cURL
183 **********************************************************/
184
185 $chHost = $host . '?xmldata=' . $xml;
186
187 $ch = curl_init($chHost);
188 if (!$ch) {
189 return self::errorExit(9004, 'Could not initiate connection to payment gateway');
190 }
191
192 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'verifySSL') ? 2 : 0);
193 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'verifySSL'));
194 // return the result on success, FALSE on failure
195 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
196 curl_setopt($ch, CURLOPT_TIMEOUT, 36000);
197 // set this for debugging -look for output in apache error log
198 //curl_setopt ($ch,CURLOPT_VERBOSE,1 );
199 // ensures any Location headers are followed
200 if (ini_get('open_basedir') == '' && ini_get('safe_mode') == 'Off') {
201 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
202 }
203
204 /**********************************************************
205 * Send the data out over the wire
206 **********************************************************/
207 $responseData = curl_exec($ch);
208
209 /**********************************************************
210 * See if we had a curl error - if so tell 'em and bail out
211 *
212 * NOTE: curl_error does not return a logical value (see its documentation), but
213 * a string, which is empty when there was no error.
214 **********************************************************/
215 if ((curl_errno($ch) > 0) || (strlen(curl_error($ch)) > 0)) {
216 curl_close($ch);
217 $errorNum = curl_errno($ch);
218 $errorDesc = curl_error($ch);
219
220 // Paranoia - in the unlikley event that 'curl' errno fails
221 if ($errorNum == 0)
222 $errorNum = 9005;
223
224 // Paranoia - in the unlikley event that 'curl' error fails
225 if (strlen($errorDesc) == 0)
226 $errorDesc = "Connection to payment gateway failed";
227 if ($errorNum = 60) {
228 return self::errorExit($errorNum, "Curl error - " . $errorDesc . " Try this link for more information http://curl.haxx.se/docs/sslcerts.html");
229 }
230
231 return self::errorExit($errorNum, "Curl error - " . $errorDesc . " your key is located at " . $key . " the url is " . $host . " xml is " . $requestxml . " processor response = " . $processorResponse);
232 }
233
234 /**********************************************************
235 * If null data returned - tell 'em and bail out
236 *
237 * NOTE: You will not necessarily get a string back, if the request failed for
238 * any reason, the return value will be the boolean false.
239 **********************************************************/
240 if (($responseData === FALSE) || (strlen($responseData) == 0)) {
241 curl_close($ch);
242 return self::errorExit(9006, "Error: Connection to payment gateway failed - no data returned.");
243 }
244
245 /**********************************************************
246 // If gateway returned no data - tell 'em and bail out
247 **********************************************************/
248 if (empty($responseData)) {
249 curl_close($ch);
250 return self::errorExit(9007, "Error: No data returned from payment gateway.");
251 }
252
253 /**********************************************************
254 // Success so far - close the curl and check the data
255 **********************************************************/
256 curl_close($ch);
257
258 /**********************************************************
259 * Payment successfully sent to gateway - process the response now
260 **********************************************************/
261
262 $processorResponse = self::decodeXMLResponse($responseData);
263 /*success in test mode returns response "APPROVED"
264 * test mode always returns trxn_id = 0
265 * fix for CRM-2566
266 **********************************************************/
267
268
269 if ($processorResponse['errorCode']) {
270 return self::errorExit(9010, "Error: [" . $processorResponse['errorCode'] . " " . $processorResponse['errorName'] . " " . $processorResponse['errorMessage'] . "] - from payment processor");
271 }
272 if ($processorResponse['ssl_result_message'] == "APPROVED") {
273 if ($this->_mode == 'test') {
274 $query = "SELECT MAX(trxn_id) FROM civicrm_contribution WHERE trxn_id LIKE 'test%'";
275 $p = array();
276 $trxn_id = strval(CRM_Core_Dao::singleValueQuery($query, $p));
277 $trxn_id = str_replace('test', '', $trxn_id);
278 $trxn_id = intval($trxn_id) + 1;
279 $params['trxn_id'] = sprintf('test%08d', $trxn_id);
280 return $params;
281 }
282 else {
283 return self::errorExit(9099, "Error: [approval code related to test transaction but mode was " . $this->_mode);
284 }
285 }
286
287 // transaction failed, print the reason
288 if ($processorResponse['ssl_result_message'] != "APPROVAL") {
289 return self::errorExit(9009, "Error: [" . $processorResponse['ssl_result_message'] . " " . $processorResponse['ssl_result'] . "] - from payment processor");
290 }
291 else {
292 /*
293 * Success !
294 */
295
296 if ($this->_mode == 'test') {}
297 else {
298 // 'trxn_id' is varchar(255) field. returned value is length 37
299 $params['trxn_id'] = $processorResponse['ssl_txn_id'];
300 }
301
302 $params['trxn_result_code'] = $processorResponse['ssl_approval_code'] . "-Cvv2:" . $processorResponse['ssl_cvv2_response'] . "-avs:" . $processorResponse['ssl_avs_response'];
303
304 return $params;
305 }
306 }
307 // end function doDirectPayment
308
309 /**
310 * Checks to see if invoice_id already exists in db
311 *
312 * @param int $invoiceId The ID to check
313 *
314 * @return bool True if ID exists, else false
315 */
316 function _checkDupe($invoiceId) {
317 $contribution = new CRM_Contribute_DAO_Contribution();
318 $contribution->invoice_id = $invoiceId;
319 return $contribution->find();
320 }
321
322 /**************************************************
323 * Produces error message and returns from class
324 **************************************************/
325 function &errorExit($errorCode = NULL, $errorMessage = NULL) {
326 $e = CRM_Core_Error::singleton();
327 if ($errorCode) {
328 $e->push($errorCode, 0, NULL, $errorMessage);
329 }
330 else {
331 $e->push(9000, 0, NULL, 'Unknown System Error.');
332 }
333 return $e;
334 }
335
336 /**************************************************
337 * NOTE: 'doTransferCheckout' not implemented
338 **************************************************/
339 function doTransferCheckout(&$params, $component) {
340 CRM_Core_Error::fatal(ts('This function is not implemented'));
341 }
342
343 /********************************************************************************************
344 * This public function checks to see if we have the right processor config values set
345 *
346 * NOTE: Called by Events and Contribute to check config params are set prior to trying
347 * register any credit card details
348 *
349 * @return null|string
350 * @internal param string $mode the mode we are operating in (live or test) - not used
351 *
352 * returns string $errorMsg if any errors found - null if OK
353 *
354 ******************************************************************************************
355 */
356 // function checkConfig( $mode ) // CiviCRM V1.9 Declaration
357 // CiviCRM V2.0 Declaration
358 function checkConfig() {
359 $errorMsg = array();
360
361 if (empty($this->_paymentProcessor['user_name'])) {
362 $errorMsg[] = ' ' . ts('ssl_merchant_id is not set for this payment processor');
363 }
364
365 if (empty($this->_paymentProcessor['url_site'])) {
366 $errorMsg[] = ' ' . ts('URL is not set for this payment processor');
367 }
368
369 if (!empty($errorMsg)) {
370 return implode('<p>', $errorMsg);
371 }
372 else {
373 return NULL;
374 }
375 }
376 //end check config
377 /**
378 * @param $requestFields
379 *
380 * @return string
381 */
382 function buildXML($requestFields) {
383 $xmlFieldLength['ssl_first_name'] = 15;
384 // credit card name
385 $xmlFieldLength['ssl_last_name'] = 15;
386 // contact name
387 $xmlFieldLength['ssl_ship_to_first_name'] = 15;
388 // contact name
389 $xmlFieldLength['ssl_ship_to_last_name'] = 15;
390 $xmlFieldLength['ssl_card_number'] = 19;
391 $xmlFieldLength['ssl_amount'] = 13;
392 $xmlFieldLength['ssl_exp_date'] = 4;
393 $xmlFieldLength['ssl_cvv2cvc2'] = 4;
394 $xmlFieldLength['ssl_cvv2cvc2_indicator'] = 1;
395 $xmlFieldLength['ssl_avs_address'] = 20;
396 $xmlFieldLength['ssl_city'] = 20;
397 $xmlFieldLength['ssl_state'] = 30;
398 $xmlFieldLength['ssl_avs_zip'] = 9;
399 $xmlFieldLength['ssl_country'] = 50;
400 $xmlFieldLength['ssl_email'] = 100;
401 // 32 character string
402 $xmlFieldLength['ssl_invoice_number'] = 25;
403 $xmlFieldLength['ssl_transaction_type'] = 20;
404 $xmlFieldLength['ssl_description'] = 255;
405 $xmlFieldLength['ssl_merchant_id'] = 15;
406 $xmlFieldLength['ssl_user_id'] = 15;
407 $xmlFieldLength['ssl_pin'] = 128;
408 $xmlFieldLength['ssl_test_mode'] = 5;
409 $xmlFieldLength['ssl_salestax'] = 10;
410 $xmlFieldLength['ssl_customer_code'] = 17;
411 $xmlFieldLength['ssl_customer_number'] = 25;
412
413 $xml = '<txn>';
414 foreach ($requestFields as $key => $value) {
415 $xml .= '<' . $key . '>' . self::tidyStringforXML($value, $xmlFieldLength[$key]) . '</' . $key . '>';
416 }
417 $xml .= '</txn>';
418 return $xml;
419 }
420
421 /**
422 * @param $value
423 * @param $fieldlength
424 *
425 * @return string
426 */
427 function tidyStringforXML($value, $fieldlength) {
428 // the xml is posted to a url so must not contain spaces etc. It also needs to be cut off at a certain
429 // length to match the processor's field length. The cut needs to be made after spaces etc are
430 // transformed but must not include a partial transformed character e.g. %20 must be in or out not half-way
431 $xmlString = substr(rawurlencode($value), 0, $fieldlength);
432 $lastPercent = strrpos($xmlString, '%');
433 if ($lastPercent > $fieldlength - 3) {
434 $xmlString = substr($xmlString, 0, $lastPercent);
435 }
436 return $xmlString;
437 }
438
439 /************************************************************************
440 * Simple function to use in place of the 'simplexml_load_string' call.
441 *
442 * It returns the NodeValue for a given NodeName
443 * or returns an empty string.
444 ************************************************************************/
445 function GetNodeValue($NodeName, &$strXML) {
446 $OpeningNodeName = "<" . $NodeName . ">";
447 $ClosingNodeName = "</" . $NodeName . ">";
448
449 $pos1 = stripos($strXML, $OpeningNodeName);
450 $pos2 = stripos($strXML, $ClosingNodeName);
451
452 if (($pos1 === FALSE) || ($pos2 === FALSE)) {
453
454 return "";
455
456 }
457
458 $pos1 += strlen($OpeningNodeName);
459 $len = $pos2 - $pos1;
460
461 $return = substr($strXML, $pos1, $len);
462 // check out rtn values for debug
463 // echo " $NodeName &nbsp &nbsp $return <br>";
464 return ($return);
465 }
466
467 /**
468 * @param $Xml
469 *
470 * @return mixed
471 */
472 function decodeXMLresponse($Xml) {
473
474 /**
475 * $xtr = simplexml_load_string($Xml) or die ("Unable to load XML string!");
476 **/
477
478 $processorResponse['ssl_result'] = self::GetNodeValue("ssl_result", $Xml);
479 $processorResponse['ssl_result_message'] = self::GetNodeValue("ssl_result_message", $Xml);
480 $processorResponse['ssl_txn_id'] = self::GetNodeValue("ssl_txn_id", $Xml);
481 $processorResponse['ssl_cvv2_response'] = self::GetNodeValue("ssl_cvv2_response", $Xml);
482 $processorResponse['ssl_avs_response'] = self::GetNodeValue("ssl_avs_response", $Xml);
483 $processorResponse['ssl_approval_code'] = self::GetNodeValue("ssl_approval_code", $Xml);
484 $processorResponse['errorCode'] = self::GetNodeValue("errorCode", $Xml);
485 $processorResponse['errorName'] = self::GetNodeValue("errorName", $Xml);
486 $processorResponse['errorMessage'] = self::GetNodeValue("errorMessage", $Xml);
487
488 return $processorResponse;
489 }
490 }
491