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