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