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