3 +----------------------------------------------------------------------------+
4 | Elavon (Nova) Virtual Merchant Core Payment Module for CiviCRM version 5 |
5 +----------------------------------------------------------------------------+
6 | Licensed to CiviCRM under the Academic Free License version 3.0 |
8 | Written & Contributed by Eileen McNaughton - Nov March 2008 |
9 +----------------------------------------------------------------------------+
12 use Civi\Payment\Exception\PaymentProcessorException
;
15 * -----------------------------------------------------------------------------------------------
16 * The basic functionality of this processor is that variables from the $params object are transformed
17 * into xml. The xml is submitted to the processor's https site
18 * using curl and the response is translated back into an array using the processor's function.
20 * If an array ($params) is returned to the calling function the values from
21 * the array are merged into the calling functions array.
23 * If an result of class error is returned it is treated as a failure. No error denotes a success. Be
24 * WARY of this when coding
26 * -----------------------------------------------------------------------------------------------
28 class CRM_Core_Payment_Elavon
extends CRM_Core_Payment
{
34 * The mode of operation: live or test.
36 * @param array $paymentProcessor
38 public function __construct($mode, &$paymentProcessor) {
41 $this->_paymentProcessor
= $paymentProcessor;
45 * Map fields to parameters.
47 * This function is set up and put here to make the mapping of fields
48 * from the params object as visually clear as possible for easy editing
50 * @param array $params
54 public function mapProcessorFieldstoParams($params) {
55 $requestFields['ssl_first_name'] = $params['billing_first_name'];
56 $requestFields['ssl_last_name'] = $params['billing_last_name'];
58 $requestFields['ssl_ship_to_first_name'] = $params['first_name'];
60 $requestFields['ssl_ship_to_last_name'] = $params['last_name'];
61 $requestFields['ssl_card_number'] = $params['credit_card_number'];
62 $requestFields['ssl_amount'] = trim($params['amount']);
63 $requestFields['ssl_exp_date'] = sprintf('%02d', (int) $params['month']) . substr($params['year'], 2, 2);
64 $requestFields['ssl_cvv2cvc2'] = $params['cvv2'];
65 // CVV field passed to processor
66 $requestFields['ssl_cvv2cvc2_indicator'] = "1";
67 $requestFields['ssl_avs_address'] = $params['street_address'];
68 $requestFields['ssl_city'] = $params['city'];
69 $requestFields['ssl_state'] = $params['state_province'];
70 $requestFields['ssl_avs_zip'] = $params['postal_code'];
71 $requestFields['ssl_country'] = $params['country'];
72 $requestFields['ssl_email'] = $params['email'];
73 // 32 character string
74 $requestFields['ssl_invoice_number'] = $params['invoiceID'];
75 $requestFields['ssl_transaction_type'] = "CCSALE";
76 $requestFields['ssl_description'] = empty($params['description']) ?
"backoffice payment" : $params['description'];
77 $requestFields['ssl_customer_number'] = substr($params['credit_card_number'], -4);
78 // Added two lines below to allow commercial cards to go through as per page 15 of Elavon developer guide
79 $requestFields['ssl_customer_code'] = '1111';
80 $requestFields['ssl_salestax'] = 0.0;
81 return $requestFields;
85 * This function sends request and receives response from the processor.
87 * @param array|PropertyBag $params
89 * @param string $component
92 * Result array (containing at least the key payment_status_id)
94 * @throws \Civi\Payment\Exception\PaymentProcessorException
96 public function doPayment(&$params, $component = 'contribute') {
97 $propertyBag = \Civi\Payment\PropertyBag
::cast($params);
98 $this->_component
= $component;
99 $statuses = CRM_Contribute_BAO_Contribution
::buildOptions('contribution_status_id', 'validate');
101 // If we have a $0 amount, skip call to processor and set payment_status to Completed.
102 // Conceivably a processor might override this - perhaps for setting up a token - but we don't
103 // have an example of that at the moment.
104 if ($propertyBag->getAmount() == 0) {
105 $result['payment_status_id'] = array_search('Completed', $statuses);
106 $result['payment_status'] = 'Completed';
110 if (isset($params['is_recur']) && $params['is_recur'] == TRUE) {
111 throw new CRM_Core_Exception(ts('Elavon - recurring payments not implemented'));
114 if (!defined('CURLOPT_SSLCERT')) {
115 throw new CRM_Core_Exception(ts('Elavon / Nova Virtual Merchant Gateway requires curl with SSL support'));
118 //Create the array of variables to be sent to the processor from the $params array
119 // passed into this function
120 $requestFields = $this->mapProcessorFieldstoParams($params);
122 // define variables for connecting with the gateway
123 $requestFields['ssl_merchant_id'] = $this->_paymentProcessor
['user_name'];
124 $requestFields['ssl_user_id'] = $this->_paymentProcessor
['password'] ??
NULL;
125 $requestFields['ssl_pin'] = $this->_paymentProcessor
['signature'] ??
NULL;
126 $host = $this->_paymentProcessor
['url_site'];
128 if ($this->_mode
=== 'test') {
129 $requestFields['ssl_test_mode'] = "TRUE";
132 // Allow further manipulation of the arguments via custom hooks ..
133 CRM_Utils_Hook
::alterPaymentProcessorParams($this, $params, $requestFields);
135 // Check to see if we have a duplicate before we send
136 if ($this->checkDupe($params['invoiceID'], CRM_Utils_Array
::value('contributionID', $params))) {
137 throw new PaymentProcessorException('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.', 9003);
140 // Convert to XML using function below
141 $xml = $this->buildXML($requestFields);
143 // Send to the payment processor using cURL
145 $chHost = $host . '?xmldata=' . $xml;
147 $ch = curl_init($chHost);
149 throw new PaymentProcessorException('Could not initiate connection to payment gateway', 9004);
152 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST
, Civi
::settings()->get('verifySSL') ?
2 : 0);
153 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER
, Civi
::settings()->get('verifySSL'));
154 // return the result on success, FALSE on failure
155 curl_setopt($ch, CURLOPT_RETURNTRANSFER
, 1);
156 curl_setopt($ch, CURLOPT_TIMEOUT
, 36000);
157 // set this for debugging -look for output in apache error log
158 //curl_setopt ($ch,CURLOPT_VERBOSE,1 );
159 // ensures any Location headers are followed
160 if (ini_get('open_basedir') == '') {
161 curl_setopt($ch, CURLOPT_FOLLOWLOCATION
, 1);
164 // Send the data out over the wire
165 $responseData = curl_exec($ch);
167 // See if we had a curl error - if so tell 'em and bail out
168 // NOTE: curl_error does not return a logical value (see its documentation), but
169 // a string, which is empty when there was no error.
170 if ((curl_errno($ch) > 0) ||
(strlen(curl_error($ch)) > 0)) {
172 $errorNum = curl_errno($ch);
173 $errorDesc = curl_error($ch);
175 // Paranoia - in the unlikley event that 'curl' errno fails
176 if ($errorNum == 0) {
180 // Paranoia - in the unlikley event that 'curl' error fails
181 if (strlen($errorDesc) == 0) {
182 $errorDesc = 'Connection to payment gateway failed';
184 throw new PaymentProcessorException('Curl error - ' . $errorDesc . ' Try this link for more information http://curl.haxx.se/docs/sslcerts.html', $errorNum);
187 // If null data returned - tell 'em and bail out
188 // NOTE: You will not necessarily get a string back, if the request failed for
189 // any reason, the return value will be the boolean false.
190 if (($responseData === FALSE) ||
(strlen($responseData) == 0)) {
192 throw new PaymentProcessorException('Error: Connection to payment gateway failed - no data returned.', 9006);
195 // If gateway returned no data - tell 'em and bail out
196 if (empty($responseData)) {
198 throw new PaymentProcessorException('Error: No data returned from payment gateway.', 9007);
201 // Success so far - close the curl and check the data
204 // Payment successfully sent to gateway - process the response now
206 $processorResponse = $this->decodeXMLresponse($responseData);
207 // success in test mode returns response "APPROVED"
208 // test mode always returns trxn_id = 0
211 if ($processorResponse['errorCode']) {
212 throw new PaymentProcessorException("Error: [" . $processorResponse['errorCode'] . " " . $processorResponse['errorName'] . " " . $processorResponse['errorMessage'] . '] - from payment processor', 9010);
214 if ($processorResponse['ssl_result_message'] === "APPROVED") {
215 if ($this->_mode
=== 'test') {
216 $query = "SELECT MAX(trxn_id) FROM civicrm_contribution WHERE trxn_id LIKE 'test%'";
217 $trxn_id = (string) CRM_Core_DAO
::singleValueQuery($query);
218 $trxn_id = (int) str_replace('test', '', $trxn_id);
220 $params['trxn_id'] = sprintf('test%08d', $trxn_id);
223 throw new PaymentProcessorException('Error: [approval code related to test transaction but mode was ' . $this->_mode
, 9099);
226 // transaction failed, print the reason
227 if ($processorResponse['ssl_result_message'] !== "APPROVAL") {
228 throw new PaymentProcessorException('Error: [' . $processorResponse['ssl_result_message'] . ' ' . $processorResponse['ssl_result'] . '] - from payment processor', 9009);
232 if ($this->_mode
!== 'test') {
233 // 'trxn_id' is varchar(255) field. returned value is length 37
234 $params['trxn_id'] = $processorResponse['ssl_txn_id'];
237 $params['trxn_result_code'] = $processorResponse['ssl_approval_code'] . "-Cvv2:" . $processorResponse['ssl_cvv2_response'] . "-avs:" . $processorResponse['ssl_avs_response'];
238 $params['payment_status_id'] = array_search('Completed', $statuses);
239 $params['payment_status'] = 'Completed';
246 * This public function checks to see if we have the right processor config values set.
248 * NOTE: Called by Events and Contribute to check config params are set prior to trying
249 * register any credit card details
251 * @return string|null
252 * $errorMsg if any errors found - null if OK
255 public function checkConfig() {
258 if (empty($this->_paymentProcessor
['user_name'])) {
259 $errorMsg[] = ' ' . ts('ssl_merchant_id is not set for this payment processor');
262 if (empty($this->_paymentProcessor
['url_site'])) {
263 $errorMsg[] = ' ' . ts('URL is not set for this payment processor');
266 if (!empty($errorMsg)) {
267 return implode('<p>', $errorMsg);
273 * @param $requestFields
277 public function buildXML($requestFields) {
278 $xmlFieldLength['ssl_first_name'] = 15;
280 $xmlFieldLength['ssl_last_name'] = 15;
282 $xmlFieldLength['ssl_ship_to_first_name'] = 15;
284 $xmlFieldLength['ssl_ship_to_last_name'] = 15;
285 $xmlFieldLength['ssl_card_number'] = 19;
286 $xmlFieldLength['ssl_amount'] = 13;
287 $xmlFieldLength['ssl_exp_date'] = 4;
288 $xmlFieldLength['ssl_cvv2cvc2'] = 4;
289 $xmlFieldLength['ssl_cvv2cvc2_indicator'] = 1;
290 $xmlFieldLength['ssl_avs_address'] = 20;
291 $xmlFieldLength['ssl_city'] = 20;
292 $xmlFieldLength['ssl_state'] = 30;
293 $xmlFieldLength['ssl_avs_zip'] = 9;
294 $xmlFieldLength['ssl_country'] = 50;
295 $xmlFieldLength['ssl_email'] = 100;
296 // 32 character string
297 $xmlFieldLength['ssl_invoice_number'] = 25;
298 $xmlFieldLength['ssl_transaction_type'] = 20;
299 $xmlFieldLength['ssl_description'] = 255;
300 $xmlFieldLength['ssl_merchant_id'] = 15;
301 $xmlFieldLength['ssl_user_id'] = 15;
302 $xmlFieldLength['ssl_pin'] = 128;
303 $xmlFieldLength['ssl_test_mode'] = 5;
304 $xmlFieldLength['ssl_salestax'] = 10;
305 $xmlFieldLength['ssl_customer_code'] = 17;
306 $xmlFieldLength['ssl_customer_number'] = 25;
309 foreach ($requestFields as $key => $value) {
310 //dev/core/966 Don't send email through the urlencode.
311 if ($key == 'ssl_email') {
312 $xml .= '<' . $key . '>' . substr($value, 0, $xmlFieldLength[$key]) . '</' . $key . '>';
315 $xml .= '<' . $key . '>' . self
::tidyStringforXML($value, $xmlFieldLength[$key]) . '</' . $key . '>';
324 * @param $fieldlength
328 public function tidyStringforXML($value, $fieldlength) {
329 // the xml is posted to a url so must not contain spaces etc. It also needs to be cut off at a certain
330 // length to match the processor's field length. The cut needs to be made after spaces etc are
331 // transformed but must not include a partial transformed character e.g. %20 must be in or out not half-way
332 $xmlString = substr(rawurlencode($value), 0, $fieldlength);
333 $lastPercent = strrpos($xmlString, '%');
334 if ($lastPercent > $fieldlength - 3) {
335 $xmlString = substr($xmlString, 0, $lastPercent);
341 * Simple function to use in place of the 'simplexml_load_string' call.
343 * It returns the NodeValue for a given NodeName
344 * or returns an empty string.
346 * @param string $NodeName
347 * @param string $strXML
350 public function GetNodeValue($NodeName, &$strXML) {
351 $OpeningNodeName = "<" . $NodeName . ">";
352 $ClosingNodeName = "</" . $NodeName . ">";
354 $pos1 = stripos($strXML, $OpeningNodeName);
355 $pos2 = stripos($strXML, $ClosingNodeName);
357 if (($pos1 === FALSE) ||
($pos2 === FALSE)) {
363 $pos1 +
= strlen($OpeningNodeName);
364 $len = $pos2 - $pos1;
366 $return = substr($strXML, $pos1, $len);
367 // check out rtn values for debug
368 // echo " $NodeName     $return <br>";
377 public function decodeXMLresponse($Xml) {
378 $processorResponse = [];
380 $processorResponse['ssl_result'] = self
::GetNodeValue("ssl_result", $Xml);
381 $processorResponse['ssl_result_message'] = self
::GetNodeValue("ssl_result_message", $Xml);
382 $processorResponse['ssl_txn_id'] = self
::GetNodeValue("ssl_txn_id", $Xml);
383 $processorResponse['ssl_cvv2_response'] = self
::GetNodeValue("ssl_cvv2_response", $Xml);
384 $processorResponse['ssl_avs_response'] = self
::GetNodeValue("ssl_avs_response", $Xml);
385 $processorResponse['ssl_approval_code'] = self
::GetNodeValue("ssl_approval_code", $Xml);
386 $processorResponse['errorCode'] = self
::GetNodeValue("errorCode", $Xml);
387 $processorResponse['errorName'] = self
::GetNodeValue("errorName", $Xml);
388 $processorResponse['errorMessage'] = self
::GetNodeValue("errorMessage", $Xml);
390 return $processorResponse;