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