| 1 | <?php |
| 2 | /* |
| 3 | +--------------------------------------------------------------------+ |
| 4 | | FirstData Core Payment Module for CiviCRM version 4.7 | |
| 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 | | This processor is based heavily on the Eway processor by Peter | |
| 11 | |Barwell | |
| 12 | | | |
| 13 | | | |
| 14 | +--------------------------------------------------------------------+ |
| 15 | */ |
| 16 | |
| 17 | /** |
| 18 | * Note that in order to use FirstData / LinkPoint you need a certificate (.pem) file issued by them |
| 19 | * and a store number. You can configure the path to the certificate and the store number |
| 20 | * through the front end of civiCRM. The path is as seen by the server not the url |
| 21 | * ----------------------------------------------------------------------------------------------- |
| 22 | * The basic functionality of this processor is that variables from the $params object are transformed |
| 23 | * into xml using a function provided by the processor. The xml is submitted to the processor's https site |
| 24 | * using curl and the response is translated back into an array using the processor's function. |
| 25 | * |
| 26 | * If an array ($params) is returned to the calling function it is treated as a success and the values from |
| 27 | * the array are merged into the calling functions array. |
| 28 | * |
| 29 | * If an result of class error is returned it is treated as a failure |
| 30 | * |
| 31 | * ----------------------------------------------------------------------------------------------- |
| 32 | */ |
| 33 | |
| 34 | /** |
| 35 | * From Payment processor documentation |
| 36 | * For testing purposes, you can use any of the card numbers listed below. The test card numbers |
| 37 | * will not result in any charges to the card. Use these card numbers with any expiration date in the |
| 38 | * future. |
| 39 | * Visa Level 2 - 4275330012345675 (replies with a referral message) |
| 40 | * JCB - 3566007770003510 |
| 41 | * Discover - 6011000993010978 |
| 42 | * MasterCard - 5424180279791765 |
| 43 | * Visa - 4005550000000019 or 4111111111111111 |
| 44 | * MasterCard Level 2 - 5404980000008386 |
| 45 | * Diners - 36555565010005 |
| 46 | * Amex - 372700997251009 |
| 47 | * |
| 48 | * ************************** |
| 49 | * Lines starting with CRM_Core_Error::debug_log_message output messages to files/upload/civicrm.log - you may with to comment them out once it is working satisfactorily |
| 50 | * |
| 51 | * For live testing uncomment the result field below and set the value to the response you wish to get from the payment processor |
| 52 | * ************************** |
| 53 | */ |
| 54 | class CRM_Core_Payment_FirstData extends CRM_Core_Payment { |
| 55 | # (not used, implicit in the API, might need to convert?) |
| 56 | const CHARSET = 'UFT-8'; |
| 57 | |
| 58 | /** |
| 59 | * We only need one instance of this object. So we use the singleton |
| 60 | * pattern and cache the instance in this variable |
| 61 | * |
| 62 | * @var object |
| 63 | */ |
| 64 | static private $_singleton = NULL; |
| 65 | |
| 66 | /** |
| 67 | * Constructor. |
| 68 | * |
| 69 | * @param string $mode |
| 70 | * The mode of operation: live or test. |
| 71 | * |
| 72 | * @param $paymentProcessor |
| 73 | * |
| 74 | * @return \CRM_Core_Payment_FirstData ******************************************************* |
| 75 | */ |
| 76 | public function __construct($mode, &$paymentProcessor) { |
| 77 | // live or test |
| 78 | $this->_mode = $mode; |
| 79 | $this->_paymentProcessor = $paymentProcessor; |
| 80 | } |
| 81 | |
| 82 | /** |
| 83 | * This function is set up and put here to make the mapping of fields |
| 84 | * from the params object as visually clear as possible for easy editing |
| 85 | * |
| 86 | * Comment out irrelevant fields |
| 87 | */ |
| 88 | public function mapProcessorFieldstoParams($params) { |
| 89 | /*concatenate full customer name first - code from EWAY gateway |
| 90 | */ |
| 91 | |
| 92 | $credit_card_name = $params['first_name'] . " "; |
| 93 | if (strlen($params['middle_name']) > 0) { |
| 94 | $credit_card_name .= $params['middle_name'] . " "; |
| 95 | } |
| 96 | $credit_card_name .= $params['last_name']; |
| 97 | |
| 98 | //compile array |
| 99 | |
| 100 | /********************************************************** |
| 101 | * Payment Processor field name **fields from $params array *** |
| 102 | *******************************************************************/ |
| 103 | |
| 104 | $requestFields['cardnumber'] = $params['credit_card_number']; |
| 105 | $requestFields['chargetotal'] = $params['amount']; |
| 106 | $requestFields['cardexpmonth'] = sprintf('%02d', (int) $params['month']); |
| 107 | $requestFields['cardexpyear'] = substr($params['year'], 2, 2); |
| 108 | $requestFields['cvmvalue'] = $params['cvv2']; |
| 109 | $requestFields['cvmindicator'] = "provided"; |
| 110 | $requestFields['name'] = $credit_card_name; |
| 111 | $requestFields['address1'] = $params['street_address']; |
| 112 | $requestFields['city'] = $params['city']; |
| 113 | $requestFields['state'] = $params['state_province']; |
| 114 | $requestFields['zip'] = $params['postal_code']; |
| 115 | $requestFields['country'] = $params['country']; |
| 116 | $requestFields['email'] = $params['email']; |
| 117 | $requestFields['ip'] = $params['ip_address']; |
| 118 | $requestFields['transactionorigin'] = "Eci"; |
| 119 | #32 character string |
| 120 | $requestFields['invoice_number'] = $params['invoiceID']; |
| 121 | $requestFields['ordertype'] = 'Sale'; |
| 122 | $requestFields['comments'] = $params['description']; |
| 123 | //**********************set 'result' for live testing ************************** |
| 124 | // $requestFields[ 'result' ] = ""; #set to "Good", "Decline" or "Duplicate" |
| 125 | // $requestFields[ '' ] = $params[ 'qfKey' ]; |
| 126 | // $requestFields[ '' ] = $params[ 'amount_other' ]; |
| 127 | // $requestFields[ '' ] = $params[ 'billing_first_name' ]; |
| 128 | // $requestFields[ '' ] = $params[ 'billing_middle_name' ]; |
| 129 | // $requestFields[ '' ] = $params[ 'billing_last_name' ]; |
| 130 | |
| 131 | // $requestFields[ '' ] = $params[ 'contributionType_name' ]; |
| 132 | // $requestFields[ '' ] = $params[ 'contributionPageID' ]; |
| 133 | // $requestFields[ '' ] = $params[ 'contributionType_accounting_code' ]; |
| 134 | // $requestFields[ '' ] = $params['amount_level' ]; |
| 135 | // $requestFields[ '' ] = $params['credit_card_type' ]; |
| 136 | // $requestFields[ 'addrnum' ] = numeric portion of street address - not yet implemented |
| 137 | // $requestFields[ 'taxexempt' ] taxexempt status (Y or N) - not implemented |
| 138 | |
| 139 | return $requestFields; |
| 140 | } |
| 141 | |
| 142 | /** |
| 143 | * This function sends request and receives response from |
| 144 | * the processor |
| 145 | */ |
| 146 | public function doDirectPayment(&$params) { |
| 147 | if ($params['is_recur'] == TRUE) { |
| 148 | CRM_Core_Error::fatal(ts('%1 - recurring payments not implemented', array(1 => $paymentProcessor))); |
| 149 | } |
| 150 | |
| 151 | if (!defined('CURLOPT_SSLCERT')) { |
| 152 | CRM_Core_Error::fatal(ts('%1 - Gateway requires curl with SSL support', array(1 => $paymentProcessor))); |
| 153 | } |
| 154 | |
| 155 | /********************************************************** |
| 156 | * Create the array of variables to be sent to the processor from the $params array |
| 157 | * passed into this function |
| 158 | **********************************************************/ |
| 159 | $requestFields = self::mapProcessorFieldstoParams($params); |
| 160 | |
| 161 | /********************************************************** |
| 162 | * create FirstData request object |
| 163 | **********************************************************/ |
| 164 | require_once 'FirstData/lphp.php'; |
| 165 | // $mylphp=new lphp; |
| 166 | |
| 167 | /********************************************************** |
| 168 | * define variables for connecting with the gateway |
| 169 | **********************************************************/ |
| 170 | |
| 171 | # Name and location of certificate file |
| 172 | $key = $this->_paymentProcessor['password']; |
| 173 | # Your store number |
| 174 | $requestFields["configfile"] = $this->_paymentProcessor['user_name']; |
| 175 | $port = "1129"; |
| 176 | $host = $this->_paymentProcessor['url_site'] . ":" . $port . "/LSGSXML"; |
| 177 | |
| 178 | //---------------------------------------------------------------------------------------------------- |
| 179 | // Check to see if we have a duplicate before we send |
| 180 | //---------------------------------------------------------------------------------------------------- |
| 181 | if ($this->checkDupe($params['invoiceID'], CRM_Utils_Array::value('contributionID', $params))) { |
| 182 | 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 from eWAY. 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.'); |
| 183 | } |
| 184 | //---------------------------------------------------------------------------------------------------- |
| 185 | // Convert to XML using function provided by payment processor |
| 186 | //---------------------------------------------------------------------------------------------------- |
| 187 | $requestxml = lphp::buildXML($requestFields); |
| 188 | |
| 189 | /*---------------------------------------------------------------------------------------------------- |
| 190 | // Send to the payment information using cURL |
| 191 | /---------------------------------------------------------------------------------------------------- |
| 192 | */ |
| 193 | |
| 194 | $ch = curl_init($host); |
| 195 | if (!$ch) { |
| 196 | return self::errorExit(9004, 'Could not initiate connection to payment gateway'); |
| 197 | } |
| 198 | |
| 199 | curl_setopt($ch, CURLOPT_POST, 1); |
| 200 | curl_setopt($ch, CURLOPT_POSTFIELDS, $requestxml); |
| 201 | curl_setopt($ch, CURLOPT_SSLCERT, $key); |
| 202 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'verifySSL') ? 2 : 0); |
| 203 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'verifySSL')); |
| 204 | // return the result on success, FALSE on failure |
| 205 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); |
| 206 | curl_setopt($ch, CURLOPT_TIMEOUT, 36000); |
| 207 | // ensures any Location headers are followed |
| 208 | if (ini_get('open_basedir') == '' && ini_get('safe_mode') == 'Off') { |
| 209 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); |
| 210 | } |
| 211 | |
| 212 | // Send the data out over the wire |
| 213 | //-------------------------------- |
| 214 | $responseData = curl_exec($ch); |
| 215 | |
| 216 | //---------------------------------------------------------------------------------------------------- |
| 217 | // See if we had a curl error - if so tell 'em and bail out |
| 218 | // |
| 219 | // NOTE: curl_error does not return a logical value (see its documentation), but |
| 220 | // a string, which is empty when there was no error. |
| 221 | //---------------------------------------------------------------------------------------------------- |
| 222 | if ((curl_errno($ch) > 0) || (strlen(curl_error($ch)) > 0)) { |
| 223 | $errorNum = curl_errno($ch); |
| 224 | $errorDesc = curl_error($ch); |
| 225 | |
| 226 | // Paranoia - in the unlikley event that 'curl' errno fails |
| 227 | if ($errorNum == 0) { |
| 228 | $errorNum = 9005; |
| 229 | } |
| 230 | |
| 231 | // Paranoia - in the unlikley event that 'curl' error fails |
| 232 | if (strlen($errorDesc) == 0) { |
| 233 | $errorDesc = "Connection to payment gateway failed"; |
| 234 | } |
| 235 | if ($errorNum == 60) { |
| 236 | return self::errorExit($errorNum, "Curl error - " . $errorDesc . " Try this link for more information http://curl.haxx.se/docs/sslcerts.html"); |
| 237 | } |
| 238 | |
| 239 | return self::errorExit($errorNum, "Curl error - " . $errorDesc . " your key is located at " . $key . " the url is " . $host . " xml is " . $requestxml . " processor response = " . $processorResponse); |
| 240 | } |
| 241 | |
| 242 | //---------------------------------------------------------------------------------------------------- |
| 243 | // If null data returned - tell 'em and bail out |
| 244 | // |
| 245 | // NOTE: You will not necessarily get a string back, if the request failed for |
| 246 | // any reason, the return value will be the boolean false. |
| 247 | //---------------------------------------------------------------------------------------------------- |
| 248 | if (($responseData === FALSE) || (strlen($responseData) == 0)) { |
| 249 | return self::errorExit(9006, "Error: Connection to payment gateway failed - no data returned."); |
| 250 | } |
| 251 | |
| 252 | //---------------------------------------------------------------------------------------------------- |
| 253 | // If gateway returned no data - tell 'em and bail out |
| 254 | //---------------------------------------------------------------------------------------------------- |
| 255 | if (empty($responseData)) { |
| 256 | return self::errorExit(9007, "Error: No data returned from payment gateway."); |
| 257 | } |
| 258 | |
| 259 | //---------------------------------------------------------------------------------------------------- |
| 260 | // Success so far - close the curl and check the data |
| 261 | //---------------------------------------------------------------------------------------------------- |
| 262 | curl_close($ch); |
| 263 | |
| 264 | //---------------------------------------------------------------------------------------------------- |
| 265 | // Payment successfully sent to gateway - process the response now |
| 266 | //---------------------------------------------------------------------------------------------------- |
| 267 | // |
| 268 | $processorResponse = lphp::decodeXML($responseData); |
| 269 | |
| 270 | // transaction failed, print the reason |
| 271 | if ($processorResponse["r_approved"] != "APPROVED") { |
| 272 | return self::errorExit(9009, "Error: [" . $processorResponse['r_error'] . "] - from payment processor"); |
| 273 | } |
| 274 | else { |
| 275 | |
| 276 | //----------------------------------------------------------------------------------------------------- |
| 277 | // Cross-Check - the unique 'TrxnReference' we sent out should match the just received 'TrxnReference' |
| 278 | // |
| 279 | // this section not used as the processor doesn't appear to pass back our invoice no. Code in eWay model if |
| 280 | // used later |
| 281 | //----------------------------------------------------------------------------------------------------- |
| 282 | |
| 283 | //============= |
| 284 | // Success ! |
| 285 | //============= |
| 286 | $params['trxn_result_code'] = $processorResponse['r_message']; |
| 287 | $params['trxn_id'] = $processorResponse['r_ref']; |
| 288 | CRM_Core_Error::debug_log_message("r_authresponse " . $processorResponse['r_authresponse']); |
| 289 | CRM_Core_Error::debug_log_message("r_code " . $processorResponse['r_code']); |
| 290 | CRM_Core_Error::debug_log_message("r_tdate " . $processorResponse['r_tdate']); |
| 291 | CRM_Core_Error::debug_log_message("r_avs " . $processorResponse['r_avs']); |
| 292 | CRM_Core_Error::debug_log_message("r_ordernum " . $processorResponse['r_ordernum']); |
| 293 | CRM_Core_Error::debug_log_message("r_error " . $processorResponse['r_error']); |
| 294 | CRM_Core_Error::debug_log_message("csp " . $processorResponse['r_csp']); |
| 295 | CRM_Core_Error::debug_log_message("r_message " . $processorResponse['r_message']); |
| 296 | CRM_Core_Error::debug_log_message("r_ref " . $processorResponse['r_ref']); |
| 297 | CRM_Core_Error::debug_log_message("r_time " . $processorResponse['r_time']); |
| 298 | return $params; |
| 299 | } |
| 300 | } |
| 301 | // end function doDirectPayment |
| 302 | |
| 303 | /** |
| 304 | * Produces error message and returns from class. |
| 305 | */ |
| 306 | public function &errorExit($errorCode = NULL, $errorMessage = NULL) { |
| 307 | $e = CRM_Core_Error::singleton(); |
| 308 | |
| 309 | if ($errorCode) { |
| 310 | $e->push($errorCode, 0, NULL, $errorMessage); |
| 311 | } |
| 312 | else { |
| 313 | $e->push(9000, 0, NULL, 'Unknown System Error.'); |
| 314 | } |
| 315 | return $e; |
| 316 | } |
| 317 | |
| 318 | /** |
| 319 | * This public function checks to see if we have the right processor config values set. |
| 320 | * |
| 321 | * NOTE: Called by Events and Contribute to check config params are set prior to trying |
| 322 | * register any credit card details |
| 323 | * |
| 324 | * @return null|string |
| 325 | * @internal param string $mode the mode we are operating in (live or test) - not used |
| 326 | * |
| 327 | * returns string $errorMsg if any errors found - null if OK |
| 328 | * |
| 329 | * function checkConfig( $mode ) CiviCRM V1.9 Declaration |
| 330 | * CiviCRM V2.0 Declaration |
| 331 | */ |
| 332 | public function checkConfig() { |
| 333 | $errorMsg = array(); |
| 334 | |
| 335 | if (empty($this->_paymentProcessor['user_name'])) { |
| 336 | $errorMsg[] = ts(' Store Name is not set for this payment processor'); |
| 337 | } |
| 338 | |
| 339 | if (empty($this->_paymentProcessor['url_site'])) { |
| 340 | $errorMsg[] = ts(' URL is not set for this payment processor'); |
| 341 | } |
| 342 | |
| 343 | if (!empty($errorMsg)) { |
| 344 | return implode('<p>', $errorMsg); |
| 345 | } |
| 346 | else { |
| 347 | return NULL; |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | } |
| 352 | // end class CRM_Core_Payment_FirstData |