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