3 +----------------------------------------------------------------------------+
4 | Elavon (Nova) Virtual Merchant Core Payment Module for CiviCRM version 4.4 |
5 +----------------------------------------------------------------------------+
6 | Licensed to CiviCRM under the Academic Free License version 3.0 |
8 | Written & Contributed by Eileen McNaughton - Nov March 2008 |
9 +----------------------------------------------------------------------------+
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.
18 If an array ($params) is returned to the calling function the values from
19 the array are merged into the calling functions array.
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
24 -----------------------------------------------------------------------------------------------
26 class CRM_Core_Payment_Elavon
extends CRM_Core_Payment
{
27 // (not used, implicit in the API, might need to convert?)
32 * We only need one instance of this object. So we use the singleton
33 * pattern and cache the instance in this variable
38 static private $_singleton = NULL;
40 /**********************************************************
43 * @param string $mode the mode of operation: live or test
46 **********************************************************/ function __construct($mode, &$paymentProcessor) {
49 $this->_paymentProcessor
= $paymentProcessor;
50 $this->_processorName
= ts('Elavon');
54 * singleton function used to manage this object
56 * @param string $mode the mode of operation: live or test
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);
67 return self
::$_singleton[$processorName];
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
74 * Comment out irrelevant fields
75 **********************************************************/
76 function mapProcessorFieldstoParams($params) {
78 /**********************************************************
80 * Payment Processor field name fields from $params array
81 **********************************************************/
83 $requestFields['ssl_first_name'] = $params['billing_first_name'];
85 //$requestFields['ssl_middle_name'] = $params['billing_middle_name'];
87 $requestFields['ssl_last_name'] = $params['billing_last_name'];
89 $requestFields['ssl_ship_to_first_name'] = $params['first_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;
114 /************************************************************************************
115 * Fields available from civiCRM not implemented for Elavon
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;
129 /**********************************************************
130 * This function sends request and receives response from
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'));
138 if (!defined('CURLOPT_SSLCERT')) {
139 CRM_Core_Error
::fatal(ts('Elavon / Nova Virtual Merchant Gateway requires curl with SSL support'));
143 *Create the array of variables to be sent to the processor from the $params array
144 * passed into this function
147 $requestFields = self
::mapProcessorFieldstoParams($params);
150 * define variables for connecting with the gateway
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'];
158 if ($this->_mode
== "test") {
159 $requestFields['ssl_test_mode'] = "TRUE";
162 // Allow further manipulation of the arguments via custom hooks ..
163 CRM_Utils_Hook
::alterPaymentProcessorParams($this, $params, $requestFields);
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.');
172 /**********************************************************
173 * Convert to XML using function below
174 **********************************************************/
175 $xml = self
::buildXML($requestFields);
177 /**********************************************************
178 * Send to the payment processor using cURL
179 **********************************************************/
181 $chHost = $host . '?xmldata=' . $xml;
183 $ch = curl_init($chHost);
185 return self
::errorExit(9004, 'Could not initiate connection to payment gateway');
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 curl_setopt($ch, CURLOPT_FOLLOWLOCATION
, 1);
198 /**********************************************************
199 * Send the data out over the wire
200 **********************************************************/
201 $responseData = curl_exec($ch);
203 /**********************************************************
204 * See if we had a curl error - if so tell 'em and bail out
206 * NOTE: curl_error does not return a logical value (see its documentation), but
207 * a string, which is empty when there was no error.
208 **********************************************************/
209 if ((curl_errno($ch) > 0) ||
(strlen(curl_error($ch)) > 0)) {
211 $errorNum = curl_errno($ch);
212 $errorDesc = curl_error($ch);
214 // Paranoia - in the unlikley event that 'curl' errno fails
218 // Paranoia - in the unlikley event that 'curl' error fails
219 if (strlen($errorDesc) == 0)
220 $errorDesc = "Connection to payment gateway failed";
221 if ($errorNum = 60) {
222 return self
::errorExit($errorNum, "Curl error - " . $errorDesc . " Try this link for more information http://curl.haxx.se/docs/sslcerts.html");
225 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 * If null data returned - tell 'em and bail out
231 * NOTE: You will not necessarily get a string back, if the request failed for
232 * any reason, the return value will be the boolean false.
233 **********************************************************/
234 if (($responseData === FALSE) ||
(strlen($responseData) == 0)) {
236 return self
::errorExit(9006, "Error: Connection to payment gateway failed - no data returned.");
239 /**********************************************************
240 // If gateway returned no data - tell 'em and bail out
241 **********************************************************/
242 if (empty($responseData)) {
244 return self
::errorExit(9007, "Error: No data returned from payment gateway.");
247 /**********************************************************
248 // Success so far - close the curl and check the data
249 **********************************************************/
252 /**********************************************************
253 * Payment successfully sent to gateway - process the response now
254 **********************************************************/
256 $processorResponse = self
::decodeXMLResponse($responseData);
257 /*success in test mode returns response "APPROVED"
258 * test mode always returns trxn_id = 0
260 **********************************************************/
263 if ($processorResponse['errorCode']) {
264 return self
::errorExit(9010, "Error: [" . $processorResponse['errorCode'] . " " . $processorResponse['errorName'] . " " . $processorResponse['errorMessage'] . "] - from payment processor");
266 if ($processorResponse['ssl_result_message'] == "APPROVED") {
267 if ($this->_mode
== 'test') {
268 $query = "SELECT MAX(trxn_id) FROM civicrm_contribution WHERE trxn_id LIKE 'test%'";
270 $trxn_id = strval(CRM_Core_Dao
::singleValueQuery($query, $p));
271 $trxn_id = str_replace('test', '', $trxn_id);
272 $trxn_id = intval($trxn_id) +
1;
273 $params['trxn_id'] = sprintf('test%08d', $trxn_id);
277 return self
::errorExit(9099, "Error: [approval code related to test transaction but mode was " . $this->_mode
);
281 // transaction failed, print the reason
282 if ($processorResponse['ssl_result_message'] != "APPROVAL") {
283 return self
::errorExit(9009, "Error: [" . $processorResponse['ssl_result_message'] . " " . $processorResponse['ssl_result'] . "] - from payment processor");
290 if ($this->_mode
== 'test') {}
292 // 'trxn_id' is varchar(255) field. returned value is length 37
293 $params['trxn_id'] = $processorResponse['ssl_txn_id'];
296 $params['trxn_result_code'] = $processorResponse['ssl_approval_code'] . "-Cvv2:" . $processorResponse['ssl_cvv2_response'] . "-avs:" . $processorResponse['ssl_avs_response'];
301 // end function doDirectPayment
304 * Checks to see if invoice_id already exists in db
306 * @param int $invoiceId The ID to check
308 * @return bool True if ID exists, else false
310 function _checkDupe($invoiceId) {
311 $contribution = new CRM_Contribute_DAO_Contribution();
312 $contribution->invoice_id
= $invoiceId;
313 return $contribution->find();
316 /**************************************************
317 * Produces error message and returns from class
318 **************************************************/
319 function &errorExit($errorCode = NULL, $errorMessage = NULL) {
320 $e = CRM_Core_Error
::singleton();
322 $e->push($errorCode, 0, NULL, $errorMessage);
325 $e->push(9000, 0, NULL, 'Unknown System Error.');
330 /**************************************************
331 * NOTE: 'doTransferCheckout' not implemented
332 **************************************************/
333 function doTransferCheckout(&$params, $component) {
334 CRM_Core_Error
::fatal(ts('This function is not implemented'));
337 /********************************************************************************************
338 * This public function checks to see if we have the right processor config values set
340 * NOTE: Called by Events and Contribute to check config params are set prior to trying
341 * register any credit card details
343 * @param string $mode the mode we are operating in (live or test) - not used
345 * returns string $errorMsg if any errors found - null if OK
347 ********************************************************************************************/
348 // function checkConfig( $mode ) // CiviCRM V1.9 Declaration
349 // CiviCRM V2.0 Declaration
350 function checkConfig() {
353 if (empty($this->_paymentProcessor
['user_name'])) {
354 $errorMsg[] = ' ' . ts('ssl_merchant_id is not set for this payment processor');
357 if (empty($this->_paymentProcessor
['url_site'])) {
358 $errorMsg[] = ' ' . ts('URL is not set for this payment processor');
361 if (!empty($errorMsg)) {
362 return implode('<p>', $errorMsg);
369 function buildXML($requestFields) {
370 $xmlFieldLength['ssl_first_name'] = 15;
372 $xmlFieldLength['ssl_last_name'] = 15;
374 $xmlFieldLength['ssl_ship_to_first_name'] = 15;
376 $xmlFieldLength['ssl_ship_to_last_name'] = 15;
377 $xmlFieldLength['ssl_card_number'] = 19;
378 $xmlFieldLength['ssl_amount'] = 13;
379 $xmlFieldLength['ssl_exp_date'] = 4;
380 $xmlFieldLength['ssl_cvv2cvc2'] = 4;
381 $xmlFieldLength['ssl_cvv2cvc2_indicator'] = 1;
382 $xmlFieldLength['ssl_avs_address'] = 20;
383 $xmlFieldLength['ssl_city'] = 20;
384 $xmlFieldLength['ssl_state'] = 30;
385 $xmlFieldLength['ssl_avs_zip'] = 9;
386 $xmlFieldLength['ssl_country'] = 50;
387 $xmlFieldLength['ssl_email'] = 100;
388 // 32 character string
389 $xmlFieldLength['ssl_invoice_number'] = 25;
390 $xmlFieldLength['ssl_transaction_type'] = 20;
391 $xmlFieldLength['ssl_description'] = 255;
392 $xmlFieldLength['ssl_merchant_id'] = 15;
393 $xmlFieldLength['ssl_user_id'] = 15;
394 $xmlFieldLength['ssl_pin'] = 128;
395 $xmlFieldLength['ssl_test_mode'] = 5;
396 $xmlFieldLength['ssl_salestax'] = 10;
397 $xmlFieldLength['ssl_customer_code'] = 17;
398 $xmlFieldLength['ssl_customer_number'] = 25;
401 foreach ($requestFields as $key => $value) {
402 $xml .= '<' . $key . '>' . self
::tidyStringforXML($value, $xmlFieldLength[$key]) . '</' . $key . '>';
408 function tidyStringforXML($value, $fieldlength) {
409 // the xml is posted to a url so must not contain spaces etc. It also needs to be cut off at a certain
410 // length to match the processor's field length. The cut needs to be made after spaces etc are
411 // transformed but must not include a partial transformed character e.g. %20 must be in or out not half-way
412 $xmlString = substr(rawurlencode($value), 0, $fieldlength);
413 $lastPercent = strrpos($xmlString, '%');
414 if ($lastPercent > $fieldlength - 3) {
415 $xmlString = substr($xmlString, 0, $lastPercent);
420 /************************************************************************
421 * Simple function to use in place of the 'simplexml_load_string' call.
423 * It returns the NodeValue for a given NodeName
424 * or returns an empty string.
425 ************************************************************************/
426 function GetNodeValue($NodeName, &$strXML) {
427 $OpeningNodeName = "<" . $NodeName . ">";
428 $ClosingNodeName = "</" . $NodeName . ">";
430 $pos1 = stripos($strXML, $OpeningNodeName);
431 $pos2 = stripos($strXML, $ClosingNodeName);
433 if (($pos1 === FALSE) ||
($pos2 === FALSE)) {
439 $pos1 +
= strlen($OpeningNodeName);
440 $len = $pos2 - $pos1;
442 $return = substr($strXML, $pos1, $len);
443 // check out rtn values for debug
444 // echo " $NodeName     $return <br>";
448 function decodeXMLresponse($Xml) {
451 * $xtr = simplexml_load_string($Xml) or die ("Unable to load XML string!");
454 $processorResponse['ssl_result'] = self
::GetNodeValue("ssl_result", $Xml);
455 $processorResponse['ssl_result_message'] = self
::GetNodeValue("ssl_result_message", $Xml);
456 $processorResponse['ssl_txn_id'] = self
::GetNodeValue("ssl_txn_id", $Xml);
457 $processorResponse['ssl_cvv2_response'] = self
::GetNodeValue("ssl_cvv2_response", $Xml);
458 $processorResponse['ssl_avs_response'] = self
::GetNodeValue("ssl_avs_response", $Xml);
459 $processorResponse['ssl_approval_code'] = self
::GetNodeValue("ssl_approval_code", $Xml);
460 $processorResponse['errorCode'] = self
::GetNodeValue("errorCode", $Xml);
461 $processorResponse['errorName'] = self
::GetNodeValue("errorName", $Xml);
462 $processorResponse['errorMessage'] = self
::GetNodeValue("errorMessage", $Xml);
464 return $processorResponse;