ad682c9762dcaa7bcfbafdfb79b655310ce85f5c
3 +----------------------------------------------------------------------------+
4 | Elavon (Nova) Virtual Merchant Core Payment Module for CiviCRM version 4.6 |
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 /**********************************************************
44 * The mode of operation: live or test.
46 * @param $paymentProcessor
48 * @return \CRM_Core_Payment_Elavon *******************************************************
50 public function __construct($mode, &$paymentProcessor) {
53 $this->_paymentProcessor
= $paymentProcessor;
54 $this->_processorName
= ts('Elavon');
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
61 * Comment out irrelevant fields
62 **********************************************************/
63 public function mapProcessorFieldstoParams($params) {
65 /**********************************************************
67 * Payment Processor field name fields from $params array
68 **********************************************************/
70 $requestFields['ssl_first_name'] = $params['billing_first_name'];
72 //$requestFields['ssl_middle_name'] = $params['billing_middle_name'];
74 $requestFields['ssl_last_name'] = $params['billing_last_name'];
76 $requestFields['ssl_ship_to_first_name'] = $params['first_name'];
78 $requestFields['ssl_ship_to_last_name'] = $params['last_name'];
79 $requestFields['ssl_card_number'] = $params['credit_card_number'];
80 $requestFields['ssl_amount'] = trim($params['amount']);
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";
94 $requestFields['ssl_description'] = empty($params['description']) ?
"backoffice payment" : $params['description'];
95 $requestFields['ssl_customer_number'] = substr($params['credit_card_number'], -4);
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;
100 /************************************************************************************
101 * Fields available from civiCRM not implemented for Elavon
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;
115 /**********************************************************
116 * This function sends request and receives response from
118 **********************************************************/
119 public function doDirectPayment(&$params) {
120 if (isset($params['is_recur']) && $params['is_recur'] == TRUE) {
121 CRM_Core_Error
::fatal(ts('Elavon - recurring payments not implemented'));
124 if (!defined('CURLOPT_SSLCERT')) {
125 CRM_Core_Error
::fatal(ts('Elavon / Nova Virtual Merchant Gateway requires curl with SSL support'));
129 *Create the array of variables to be sent to the processor from the $params array
130 * passed into this function
133 $requestFields = self
::mapProcessorFieldstoParams($params);
136 * define variables for connecting with the gateway
139 $requestFields['ssl_merchant_id'] = $this->_paymentProcessor
['user_name'];
140 $requestFields['ssl_user_id'] = $this->_paymentProcessor
['password'];
141 $requestFields['ssl_pin'] = $this->_paymentProcessor
['signature'];
142 $host = $this->_paymentProcessor
['url_site'];
144 if ($this->_mode
== "test") {
145 $requestFields['ssl_test_mode'] = "TRUE";
148 // Allow further manipulation of the arguments via custom hooks ..
149 CRM_Utils_Hook
::alterPaymentProcessorParams($this, $params, $requestFields);
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.');
158 /**********************************************************
159 * Convert to XML using function below
160 **********************************************************/
161 $xml = self
::buildXML($requestFields);
163 /**********************************************************
164 * Send to the payment processor using cURL
165 **********************************************************/
167 $chHost = $host . '?xmldata=' . $xml;
169 $ch = curl_init($chHost);
171 return self
::errorExit(9004, 'Could not initiate connection to payment gateway');
174 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST
, CRM_Core_BAO_Setting
::getItem(CRM_Core_BAO_Setting
::SYSTEM_PREFERENCES_NAME
, 'verifySSL') ?
2 : 0);
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
182 if (ini_get('open_basedir') == '' && ini_get('safe_mode') == 'Off') {
183 curl_setopt($ch, CURLOPT_FOLLOWLOCATION
, 1);
186 /**********************************************************
187 * Send the data out over the wire
188 **********************************************************/
189 $responseData = curl_exec($ch);
191 /**********************************************************
192 * See if we had a curl error - if so tell 'em and bail out
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)) {
199 $errorNum = curl_errno($ch);
200 $errorDesc = curl_error($ch);
202 // Paranoia - in the unlikley event that 'curl' errno fails
203 if ($errorNum == 0) {
207 // Paranoia - in the unlikley event that 'curl' error fails
208 if (strlen($errorDesc) == 0) {
209 $errorDesc = "Connection to payment gateway failed";
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");
215 return self
::errorExit($errorNum, "Curl error - " . $errorDesc . " your key is located at " . $key . " the url is " . $host . " xml is " . $requestxml . " processor response = " . $processorResponse);
218 /**********************************************************
219 * If null data returned - tell 'em and bail out
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)) {
226 return self
::errorExit(9006, "Error: Connection to payment gateway failed - no data returned.");
229 /**********************************************************
230 * // If gateway returned no data - tell 'em and bail out
231 **********************************************************/
232 if (empty($responseData)) {
234 return self
::errorExit(9007, "Error: No data returned from payment gateway.");
237 /**********************************************************
238 * // Success so far - close the curl and check the data
239 **********************************************************/
242 /**********************************************************
243 * Payment successfully sent to gateway - process the response now
244 **********************************************************/
246 $processorResponse = self
::decodeXMLResponse($responseData);
247 /*success in test mode returns response "APPROVED"
248 * test mode always returns trxn_id = 0
250 **********************************************************/
252 if ($processorResponse['errorCode']) {
253 return self
::errorExit(9010, "Error: [" . $processorResponse['errorCode'] . " " . $processorResponse['errorName'] . " " . $processorResponse['errorMessage'] . "] - from payment processor");
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%'";
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);
266 return self
::errorExit(9099, "Error: [approval code related to test transaction but mode was " . $this->_mode
);
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");
279 if ($this->_mode
== 'test') {
282 // 'trxn_id' is varchar(255) field. returned value is length 37
283 $params['trxn_id'] = $processorResponse['ssl_txn_id'];
286 $params['trxn_result_code'] = $processorResponse['ssl_approval_code'] . "-Cvv2:" . $processorResponse['ssl_cvv2_response'] . "-avs:" . $processorResponse['ssl_avs_response'];
291 // end function doDirectPayment
294 * Checks to see if invoice_id already exists in db
296 * @param int $invoiceId
300 * True if ID exists, else false
302 public function _checkDupe($invoiceId) {
303 $contribution = new CRM_Contribute_DAO_Contribution();
304 $contribution->invoice_id
= $invoiceId;
305 return $contribution->find();
308 /**************************************************
309 * Produces error message and returns from class
310 **************************************************/
311 public function &errorExit($errorCode = NULL, $errorMessage = NULL) {
312 $e = CRM_Core_Error
::singleton();
314 $e->push($errorCode, 0, NULL, $errorMessage);
317 $e->push(9000, 0, NULL, 'Unknown System Error.');
322 /**************************************************
323 * NOTE: 'doTransferCheckout' not implemented
324 **************************************************/
325 public function doTransferCheckout(&$params, $component) {
326 CRM_Core_Error
::fatal(ts('This function is not implemented'));
329 /********************************************************************************************
330 * This public function checks to see if we have the right processor config values set
332 * NOTE: Called by Events and Contribute to check config params are set prior to trying
333 * register any credit card details
335 * @return null|string
336 * @internal param string $mode the mode we are operating in (live or test) - not used
338 * returns string $errorMsg if any errors found - null if OK
340 ******************************************************************************************
342 // function checkConfig( $mode ) // CiviCRM V1.9 Declaration
343 // CiviCRM V2.0 Declaration
344 public function checkConfig() {
347 if (empty($this->_paymentProcessor
['user_name'])) {
348 $errorMsg[] = ' ' . ts('ssl_merchant_id is not set for this payment processor');
351 if (empty($this->_paymentProcessor
['url_site'])) {
352 $errorMsg[] = ' ' . ts('URL is not set for this payment processor');
355 if (!empty($errorMsg)) {
356 return implode('<p>', $errorMsg);
364 * @param $requestFields
368 public function buildXML($requestFields) {
369 $xmlFieldLength['ssl_first_name'] = 15;
371 $xmlFieldLength['ssl_last_name'] = 15;
373 $xmlFieldLength['ssl_ship_to_first_name'] = 15;
375 $xmlFieldLength['ssl_ship_to_last_name'] = 15;
376 $xmlFieldLength['ssl_card_number'] = 19;
377 $xmlFieldLength['ssl_amount'] = 13;
378 $xmlFieldLength['ssl_exp_date'] = 4;
379 $xmlFieldLength['ssl_cvv2cvc2'] = 4;
380 $xmlFieldLength['ssl_cvv2cvc2_indicator'] = 1;
381 $xmlFieldLength['ssl_avs_address'] = 20;
382 $xmlFieldLength['ssl_city'] = 20;
383 $xmlFieldLength['ssl_state'] = 30;
384 $xmlFieldLength['ssl_avs_zip'] = 9;
385 $xmlFieldLength['ssl_country'] = 50;
386 $xmlFieldLength['ssl_email'] = 100;
387 // 32 character string
388 $xmlFieldLength['ssl_invoice_number'] = 25;
389 $xmlFieldLength['ssl_transaction_type'] = 20;
390 $xmlFieldLength['ssl_description'] = 255;
391 $xmlFieldLength['ssl_merchant_id'] = 15;
392 $xmlFieldLength['ssl_user_id'] = 15;
393 $xmlFieldLength['ssl_pin'] = 128;
394 $xmlFieldLength['ssl_test_mode'] = 5;
395 $xmlFieldLength['ssl_salestax'] = 10;
396 $xmlFieldLength['ssl_customer_code'] = 17;
397 $xmlFieldLength['ssl_customer_number'] = 25;
400 foreach ($requestFields as $key => $value) {
401 $xml .= '<' . $key . '>' . self
::tidyStringforXML($value, $xmlFieldLength[$key]) . '</' . $key . '>';
409 * @param $fieldlength
413 public function tidyStringforXML($value, $fieldlength) {
414 // the xml is posted to a url so must not contain spaces etc. It also needs to be cut off at a certain
415 // length to match the processor's field length. The cut needs to be made after spaces etc are
416 // transformed but must not include a partial transformed character e.g. %20 must be in or out not half-way
417 $xmlString = substr(rawurlencode($value), 0, $fieldlength);
418 $lastPercent = strrpos($xmlString, '%');
419 if ($lastPercent > $fieldlength - 3) {
420 $xmlString = substr($xmlString, 0, $lastPercent);
425 /************************************************************************
426 * Simple function to use in place of the 'simplexml_load_string' call.
428 * It returns the NodeValue for a given NodeName
429 * or returns an empty string.
430 ************************************************************************/
431 public function GetNodeValue($NodeName, &$strXML) {
432 $OpeningNodeName = "<" . $NodeName . ">";
433 $ClosingNodeName = "</" . $NodeName . ">";
435 $pos1 = stripos($strXML, $OpeningNodeName);
436 $pos2 = stripos($strXML, $ClosingNodeName);
438 if (($pos1 === FALSE) ||
($pos2 === FALSE)) {
444 $pos1 +
= strlen($OpeningNodeName);
445 $len = $pos2 - $pos1;
447 $return = substr($strXML, $pos1, $len);
448 // check out rtn values for debug
449 // echo " $NodeName     $return <br>";
458 public function decodeXMLresponse($Xml) {
461 * $xtr = simplexml_load_string($Xml) or die ("Unable to load XML string!");
464 $processorResponse['ssl_result'] = self
::GetNodeValue("ssl_result", $Xml);
465 $processorResponse['ssl_result_message'] = self
::GetNodeValue("ssl_result_message", $Xml);
466 $processorResponse['ssl_txn_id'] = self
::GetNodeValue("ssl_txn_id", $Xml);
467 $processorResponse['ssl_cvv2_response'] = self
::GetNodeValue("ssl_cvv2_response", $Xml);
468 $processorResponse['ssl_avs_response'] = self
::GetNodeValue("ssl_avs_response", $Xml);
469 $processorResponse['ssl_approval_code'] = self
::GetNodeValue("ssl_approval_code", $Xml);
470 $processorResponse['errorCode'] = self
::GetNodeValue("errorCode", $Xml);
471 $processorResponse['errorName'] = self
::GetNodeValue("errorName", $Xml);
472 $processorResponse['errorMessage'] = self
::GetNodeValue("errorMessage", $Xml);
474 return $processorResponse;