Commit | Line | Data |
---|---|---|
6a488035 TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
f452d72c | 4 | | Copyright CiviCRM LLC. All rights reserved. | |
6a488035 | 5 | | | |
f452d72c CW |
6 | | This work is published under the GNU AGPLv3 license with some | |
7 | | permitted exceptions and without any warranty. For full license | | |
8 | | and copyright information, see https://civicrm.org/licensing | | |
6a488035 | 9 | +--------------------------------------------------------------------+ |
d25dd0ee | 10 | */ |
6a488035 | 11 | |
6a488035 | 12 | /* |
d424ffde CW |
13 | * Copyright (C) 2009 |
14 | * Licensed to CiviCRM under the Academic Free License version 3.0. | |
15 | * | |
16 | * Written and contributed by Kirkdesigns (http://www.kirkdesigns.co.uk) | |
17 | */ | |
6a488035 | 18 | |
9248c002 | 19 | use Civi\Payment\Exception\PaymentProcessorException; |
20 | ||
6a488035 TO |
21 | /** |
22 | * | |
23 | * @package CRM | |
24 | * @author Tom Kirkpatrick <tkp@kirkdesigns.co.uk> | |
6a488035 TO |
25 | */ |
26 | class CRM_Core_Payment_Realex extends CRM_Core_Payment { | |
7da04cde | 27 | const AUTH_APPROVED = '00'; |
6a488035 | 28 | |
9248c002 | 29 | protected $_mode; |
6a488035 | 30 | |
be2fb01f | 31 | protected $_params = []; |
6a488035 | 32 | |
6a488035 | 33 | /** |
fe482240 | 34 | * Constructor. |
6a488035 | 35 | * |
6a0b768e TO |
36 | * @param string $mode |
37 | * The mode of operation: live or test. | |
6a488035 | 38 | * |
9248c002 | 39 | * @param array $paymentProcessor |
6a488035 | 40 | */ |
00be9182 | 41 | public function __construct($mode, &$paymentProcessor) { |
6a488035 TO |
42 | $this->_mode = $mode; |
43 | $this->_paymentProcessor = $paymentProcessor; | |
6a488035 TO |
44 | |
45 | $this->_setParam('merchant_ref', $paymentProcessor['user_name']); | |
46 | $this->_setParam('secret', $paymentProcessor['password']); | |
47 | $this->_setParam('account', $paymentProcessor['subject']); | |
48 | ||
49 | $this->_setParam('emailCustomer', 'TRUE'); | |
50 | srand(time()); | |
51 | $this->_setParam('sequence', rand(1, 1000)); | |
52 | } | |
6a488035 | 53 | |
6a488035 | 54 | /** |
fe482240 | 55 | * Submit a payment using Advanced Integration Method. |
6a488035 | 56 | * |
2d9ca642 MW |
57 | * @param array|PropertyBag $params |
58 | * | |
59 | * @param string $component | |
6a488035 | 60 | * |
a6c01b45 | 61 | * @return array |
2d9ca642 | 62 | * Result array (containing at least the key payment_status_id) |
9248c002 | 63 | * |
64 | * @throws \Civi\Payment\Exception\PaymentProcessorException | |
6a488035 | 65 | */ |
2d9ca642 MW |
66 | public function doPayment(&$params, $component = 'contribute') { |
67 | $propertyBag = \Civi\Payment\PropertyBag::cast($params); | |
68 | $this->_component = $component; | |
69 | $statuses = CRM_Contribute_BAO_Contribution::buildOptions('contribution_status_id', 'validate'); | |
70 | ||
71 | // If we have a $0 amount, skip call to processor and set payment_status to Completed. | |
72 | // Conceivably a processor might override this - perhaps for setting up a token - but we don't | |
73 | // have an example of that at the moment. | |
74 | if ($propertyBag->getAmount() == 0) { | |
75 | $result['payment_status_id'] = array_search('Completed', $statuses); | |
76 | $result['payment_status'] = 'Completed'; | |
77 | return $result; | |
78 | } | |
6a488035 TO |
79 | |
80 | if (!defined('CURLOPT_SSLCERT')) { | |
9248c002 | 81 | throw new PaymentProcessorException(ts('RealAuth requires curl with SSL support'), 9001); |
6a488035 TO |
82 | } |
83 | ||
84 | $result = $this->setRealexFields($params); | |
85 | ||
6a488035 TO |
86 | /********************************************************** |
87 | * Check to see if we have a duplicate before we send | |
88 | **********************************************************/ | |
d253aeb8 | 89 | if ($this->checkDupe($params['invoiceID'], CRM_Utils_Array::value('contributionID', $params))) { |
9248c002 | 90 | throw new PaymentProcessorException(ts('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 Authorize.net. 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.'), 9004); |
6a488035 TO |
91 | } |
92 | ||
93 | // Create sha1 hash for request | |
353ffa53 | 94 | $hashme = "{$this->_getParam('timestamp')}.{$this->_getParam('merchant_ref')}.{$this->_getParam('order_id')}.{$this->_getParam('amount')}.{$this->_getParam('currency')}.{$this->_getParam('card_number')}"; |
6a488035 | 95 | $sha1hash = sha1($hashme); |
353ffa53 | 96 | $hashme = "$sha1hash.{$this->_getParam('secret')}"; |
6a488035 TO |
97 | $sha1hash = sha1($hashme); |
98 | ||
6a488035 TO |
99 | // Generate the request xml that is send to Realex Payments. |
100 | $request_xml = "<request type='auth' timestamp='{$this->_getParam('timestamp')}'> | |
101 | <merchantid>{$this->_getParam('merchant_ref')}</merchantid> | |
102 | <account>{$this->_getParam('account')}</account> | |
103 | <orderid>{$this->_getParam('order_id')}</orderid> | |
104 | <amount currency='{$this->_getParam('currency')}'>{$this->_getParam('amount')}</amount> | |
105 | <card> | |
106 | <number>{$this->_getParam('card_number')}</number> | |
107 | <expdate>{$this->_getParam('exp_date')}</expdate> | |
108 | <type>{$this->_getParam('card_type')}</type> | |
109 | <chname>{$this->_getParam('card_name')}</chname> | |
110 | <issueno>{$this->_getParam('issue_number')}</issueno> | |
111 | <cvn> | |
112 | <number>{$this->_getParam('cvn')}</number> | |
113 | <presind>1</presind> | |
114 | </cvn> | |
115 | </card> | |
116 | <autosettle flag='1'/> | |
117 | <sha1hash>$sha1hash</sha1hash> | |
118 | <comments> | |
119 | <comment id='1'>{$this->_getParam('comments')}</comment> | |
120 | </comments> | |
121 | <tssinfo> | |
122 | <varref>{$this->_getParam('varref')}</varref> | |
123 | </tssinfo> | |
124 | </request>"; | |
125 | ||
126 | /********************************************************** | |
127 | * Send to the payment processor using cURL | |
128 | **********************************************************/ | |
129 | ||
130 | $submit = curl_init($this->_paymentProcessor['url_site']); | |
131 | ||
132 | if (!$submit) { | |
9248c002 | 133 | throw new PaymentProcessorException(ts('Could not initiate connection to payment gateway'), 9002); |
6a488035 TO |
134 | } |
135 | ||
be2fb01f | 136 | curl_setopt($submit, CURLOPT_HTTPHEADER, ['SOAPAction: ""']); |
6a488035 TO |
137 | curl_setopt($submit, CURLOPT_RETURNTRANSFER, 1); |
138 | curl_setopt($submit, CURLOPT_TIMEOUT, 60); | |
aaffa79f | 139 | curl_setopt($submit, CURLOPT_SSL_VERIFYPEER, Civi::settings()->get('verifySSL')); |
6a488035 TO |
140 | curl_setopt($submit, CURLOPT_HEADER, 0); |
141 | ||
142 | // take caching out of the picture | |
143 | curl_setopt($submit, CURLOPT_FORBID_REUSE, 1); | |
144 | curl_setopt($submit, CURLOPT_FRESH_CONNECT, 1); | |
145 | ||
146 | // Apply the XML to our curl call | |
147 | curl_setopt($submit, CURLOPT_POST, 1); | |
148 | curl_setopt($submit, CURLOPT_POSTFIELDS, $request_xml); | |
149 | ||
150 | $response_xml = curl_exec($submit); | |
151 | ||
152 | if (!$response_xml) { | |
9248c002 | 153 | throw new PaymentProcessorException(curl_error($submit), curl_errno($submit)); |
6a488035 TO |
154 | } |
155 | ||
156 | curl_close($submit); | |
157 | ||
b44e3f84 | 158 | // Tidy up the response xml |
6a488035 TO |
159 | $response_xml = preg_replace("/[\s\t]/", " ", $response_xml); |
160 | $response_xml = preg_replace("/[\n\r]/", "", $response_xml); | |
161 | ||
162 | // Parse the response xml | |
163 | $xml_parser = xml_parser_create(); | |
164 | if (!xml_parse($xml_parser, $response_xml)) { | |
9248c002 | 165 | throw new PaymentProcessorException('XML Error', 9003); |
6a488035 TO |
166 | } |
167 | ||
168 | $response = $this->xml_parse_into_assoc($response_xml); | |
169 | $response = $response['#return']['RESPONSE']; | |
170 | ||
171 | // Log the Realex response for debugging | |
172 | // CRM_Core_Error::debug_var('REALEX --------- Response from Realex: ', $response, TRUE); | |
173 | ||
174 | // Return an error if authentication was not successful | |
175 | if ($response['RESULT'] !== self::AUTH_APPROVED) { | |
9248c002 | 176 | throw new PaymentProcessorException($this->getDisplayError($response['RESULT'], ' ' . $response['MESSAGE'])); |
6a488035 TO |
177 | } |
178 | ||
179 | // FIXME: We are using the trxn_result_code column to store all these extra details since there | |
180 | // seems to be nowhere else to put them. This is THE WRONG THING TO DO! | |
be2fb01f | 181 | $extras = [ |
6a488035 TO |
182 | 'authcode' => $response['AUTHCODE'], |
183 | 'batch_id' => $response['BATCHID'], | |
184 | 'message' => $response['MESSAGE'], | |
185 | 'trxn_result_code' => $response['RESULT'], | |
be2fb01f | 186 | ]; |
6a488035 TO |
187 | |
188 | $params['trxn_id'] = $response['PASREF']; | |
189 | $params['trxn_result_code'] = serialize($extras); | |
190 | $params['currencyID'] = $this->_getParam('currency'); | |
6a488035 | 191 | $params['fee_amount'] = 0; |
2d9ca642 MW |
192 | $params['payment_status_id'] = array_search('Completed', $statuses); |
193 | $params['payment_status'] = 'Completed'; | |
6a488035 TO |
194 | |
195 | return $params; | |
196 | } | |
197 | ||
198 | /** | |
199 | * Helper function to convert XML string to multi-dimension array. | |
200 | * | |
5a4f6742 | 201 | * @param string $xml |
6a488035 TO |
202 | * an XML string. |
203 | * | |
a6c01b45 CW |
204 | * @return array |
205 | * An array of the result with following keys: | |
6a488035 | 206 | */ |
00be9182 | 207 | public function xml_parse_into_assoc($xml) { |
be2fb01f CW |
208 | $input = []; |
209 | $result = []; | |
6a488035 TO |
210 | |
211 | $result['#error'] = FALSE; | |
212 | $result['#return'] = NULL; | |
213 | ||
214 | $xmlparser = xml_parser_create(); | |
215 | $ret = xml_parse_into_struct($xmlparser, $xml, $input); | |
216 | ||
217 | xml_parser_free($xmlparser); | |
218 | ||
219 | if (empty($input)) { | |
220 | $result['#return'] = $xml; | |
221 | } | |
222 | else { | |
223 | if ($ret > 0) { | |
224 | $result['#return'] = $this->_xml_parse($input); | |
225 | } | |
226 | else { | |
227 | $result['#error'] = ts('Error parsing XML result - error code = %1 at line %2 char %3', | |
be2fb01f | 228 | [ |
6a488035 TO |
229 | 1 => xml_get_error_code($xmlparser), |
230 | 2 => xml_get_current_line_number($xmlparser), | |
21dfd5f5 | 231 | 3 => xml_get_current_column_number($xmlparser), |
be2fb01f | 232 | ] |
6a488035 TO |
233 | ); |
234 | } | |
235 | } | |
236 | return $result; | |
237 | } | |
238 | ||
6c786a9b | 239 | /** |
9248c002 | 240 | * Private helper for xml_parse_into_assoc, to recursively parsing the result. |
241 | * | |
6c786a9b EM |
242 | * @param $input |
243 | * @param int $depth | |
244 | * | |
245 | * @return array | |
246 | */ | |
00be9182 | 247 | public function _xml_parse($input, $depth = 1) { |
be2fb01f CW |
248 | $output = []; |
249 | $children = []; | |
6a488035 TO |
250 | |
251 | foreach ($input as $data) { | |
252 | if ($data['level'] == $depth) { | |
253 | switch ($data['type']) { | |
254 | case 'complete': | |
2e1f50d6 | 255 | $output[$data['tag']] = $data['value'] ?? ''; |
6a488035 TO |
256 | break; |
257 | ||
258 | case 'open': | |
be2fb01f | 259 | $children = []; |
6a488035 TO |
260 | break; |
261 | ||
262 | case 'close': | |
263 | $output[$data['tag']] = $this->_xml_parse($children, $depth + 1); | |
264 | break; | |
265 | } | |
266 | } | |
267 | else { | |
268 | $children[] = $data; | |
269 | } | |
270 | } | |
271 | return $output; | |
272 | } | |
273 | ||
274 | /** | |
ea3ddccf | 275 | * Format the params from the form ready for sending to Realex. |
276 | * | |
277 | * Also perform some validation | |
278 | * | |
279 | * @param array $params | |
280 | * | |
9248c002 | 281 | * @throws \Civi\Payment\Exception\PaymentProcessorException |
6a488035 | 282 | */ |
00be9182 | 283 | public function setRealexFields(&$params) { |
2aa397bc | 284 | if ((int) $params['amount'] <= 0) { |
9248c002 | 285 | throw new PaymentProcessorException(ts('Amount must be positive'), 9001); |
6a488035 TO |
286 | } |
287 | ||
288 | // format amount to be in smallest possible units | |
289 | //list($bills, $pennies) = explode('.', $params['amount']); | |
290 | $this->_setParam('amount', 100 * $params['amount']); | |
291 | ||
292 | switch (strtolower($params['credit_card_type'])) { | |
293 | case 'mastercard': | |
294 | $this->_setParam('card_type', 'MC'); | |
295 | $this->_setParam('requiresIssueNumber', FALSE); | |
296 | break; | |
297 | ||
298 | case 'visa': | |
299 | $this->_setParam('card_type', 'VISA'); | |
300 | $this->_setParam('requiresIssueNumber', FALSE); | |
301 | break; | |
302 | ||
303 | case 'amex': | |
304 | $this->_setParam('card_type', 'AMEX'); | |
305 | $this->_setParam('requiresIssueNumber', FALSE); | |
306 | break; | |
307 | ||
308 | case 'laser': | |
309 | $this->_setParam('card_type', 'LASER'); | |
310 | $this->_setParam('requiresIssueNumber', FALSE); | |
311 | break; | |
312 | ||
313 | case 'maestro': | |
314 | case 'switch': | |
315 | case 'maestro/switch': | |
316 | case 'solo': | |
317 | $this->_setParam('card_type', 'SWITCH'); | |
318 | $this->_setParam('requiresIssueNumber', TRUE); | |
319 | break; | |
320 | ||
321 | default: | |
9248c002 | 322 | throw new PaymentProcessorException(ts('Credit card type not supported by Realex:') . ' ' . $params['credit_card_type'], 9001); |
6a488035 TO |
323 | } |
324 | ||
325 | // get the card holder name - cater cor customized billing forms | |
326 | if (isset($params['cardholder_name'])) { | |
327 | $credit_card_name = $params['cardholder_name']; | |
328 | } | |
329 | else { | |
330 | $credit_card_name = $params['first_name'] . " "; | |
331 | if (!empty($params['middle_name'])) { | |
332 | $credit_card_name .= $params['middle_name'] . " "; | |
333 | } | |
334 | $credit_card_name .= $params['last_name']; | |
335 | } | |
336 | ||
337 | $this->_setParam('card_name', $credit_card_name); | |
338 | $this->_setParam('card_number', str_replace(' ', '', $params['credit_card_number'])); | |
339 | $this->_setParam('cvn', $params['cvv2']); | |
340 | $this->_setParam('country', $params['country']); | |
341 | $this->_setParam('post_code', $params['postal_code']); | |
342 | $this->_setParam('order_id', $params['invoiceID']); | |
2e1f50d6 | 343 | $params['issue_number'] = ($params['issue_number'] ?? ''); |
6a488035 TO |
344 | $this->_setParam('issue_number', $params['issue_number']); |
345 | $this->_setParam('varref', $params['contributionType_name']); | |
346 | $comment = $params['description'] . ' (page id:' . $params['contributionPageID'] . ')'; | |
347 | $this->_setParam('comments', $comment); | |
6a488035 TO |
348 | |
349 | // set the currency to the default which can be overrided. | |
9248c002 | 350 | $this->_setParam('currency', CRM_Core_Config::singleton()->defaultCurrency); |
6a488035 TO |
351 | |
352 | // Format the expiry date to MMYY | |
2aa397bc | 353 | $expmonth = (string) $params['month']; |
6a488035 | 354 | $expmonth = (strlen($expmonth) === 1) ? '0' . $expmonth : $expmonth; |
353ffa53 | 355 | $expyear = substr((string) $params['year'], 2, 2); |
6a488035 TO |
356 | $this->_setParam('exp_date', $expmonth . $expyear); |
357 | ||
358 | if (isset($params['credit_card_start_date']) && (strlen($params['credit_card_start_date']['M']) !== 0) && | |
359 | (strlen($params['credit_card_start_date']['Y']) !== 0) | |
360 | ) { | |
2aa397bc | 361 | $startmonth = (string) $params['credit_card_start_date']['M']; |
6a488035 | 362 | $startmonth = (strlen($startmonth) === 1) ? '0' . $startmonth : $startmonth; |
353ffa53 | 363 | $startyear = substr((string) $params['credit_card_start_date']['Y'], 2, 2); |
6a488035 TO |
364 | $this->_setParam('start_date', $startmonth . $startyear); |
365 | } | |
366 | ||
367 | // Create timestamp | |
9248c002 | 368 | $timestamp = strftime('%Y%m%d%H%M%S'); |
6a488035 | 369 | $this->_setParam('timestamp', $timestamp); |
6a488035 TO |
370 | } |
371 | ||
6a488035 | 372 | /** |
fe482240 | 373 | * Get the value of a field if set. |
6a488035 | 374 | * |
6a0b768e TO |
375 | * @param string $field |
376 | * The field. | |
6a488035 | 377 | * |
72b3a70c CW |
378 | * @return mixed |
379 | * value of the field, or empty string if the field is | |
16b10e64 | 380 | * not set |
6a488035 | 381 | */ |
00be9182 | 382 | public function _getParam($field) { |
6a488035 TO |
383 | if (isset($this->_params[$field])) { |
384 | return $this->_params[$field]; | |
385 | } | |
9248c002 | 386 | return ''; |
6a488035 TO |
387 | } |
388 | ||
389 | /** | |
390 | * Set a field to the specified value. Value must be a scalar (int, | |
391 | * float, string, or boolean) | |
392 | * | |
393 | * @param string $field | |
394 | * @param mixed $value | |
6a488035 | 395 | */ |
00be9182 | 396 | public function _setParam($field, $value) { |
6a488035 | 397 | if (!is_scalar($value)) { |
9248c002 | 398 | return; |
6a488035 | 399 | } |
9248c002 | 400 | $this->_params[$field] = $value; |
6a488035 TO |
401 | } |
402 | ||
403 | /** | |
fe482240 | 404 | * This function checks to see if we have the right config values. |
6a488035 | 405 | * |
a6c01b45 CW |
406 | * @return string |
407 | * the error message if any | |
6a488035 | 408 | */ |
00be9182 | 409 | public function checkConfig() { |
be2fb01f | 410 | $error = []; |
6a488035 TO |
411 | if (empty($this->_paymentProcessor['user_name'])) { |
412 | $error[] = ts('Merchant ID is not set for this payment processor'); | |
413 | } | |
414 | ||
415 | if (empty($this->_paymentProcessor['password'])) { | |
416 | $error[] = ts('Secret is not set for this payment processor'); | |
417 | } | |
418 | ||
419 | if (!empty($error)) { | |
420 | return implode('<p>', $error); | |
421 | } | |
9248c002 | 422 | return NULL; |
423 | } | |
424 | ||
425 | /** | |
426 | * Get the error to display. | |
427 | * | |
428 | * @param string $errorCode | |
429 | * @param string $errorMessage | |
430 | * | |
431 | * @return string | |
432 | */ | |
433 | protected function getDisplayError($errorCode, $errorMessage): string { | |
434 | if ($errorCode === '101' || $errorCode === '102') { | |
435 | $display_error = ts('Card declined by bank. Please try with a different card.'); | |
436 | } | |
437 | elseif ($errorCode === '103') { | |
438 | $display_error = ts('Card reported lost or stolen. This incident will be reported.'); | |
439 | } | |
440 | elseif ($errorCode === '501') { | |
441 | $display_error = ts('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 for this transaction. 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.'); | |
442 | } | |
443 | elseif ($errorCode === '509') { | |
444 | $display_error = $errorMessage; | |
445 | } | |
6a488035 | 446 | else { |
9248c002 | 447 | $display_error = ts('We were unable to process your payment at this time. Please try again later.'); |
6a488035 | 448 | } |
9248c002 | 449 | return $display_error; |
6a488035 | 450 | } |
96025800 | 451 | |
6a488035 | 452 | } |