Removed misleading comment.
[trustcommerce.git] / trustcommerce.php
CommitLineData
71a6ba5c
LMM
1<?php
2/*
3 * Copyright (C) 2012
4 * Licensed to CiviCRM under the GPL v3 or higher
5 *
6 * Written and contributed by Ward Vandewege <ward@fsf.org> (http://www.fsf.org)
7 *
8 */
9
10// Define logging level (0 = off, 4 = log everything)
11define('TRUSTCOMMERCE_LOGGING_LEVEL', 4);
12
13require_once 'CRM/Core/Payment.php';
14class org_fsf_payment_trustcommerce extends CRM_Core_Payment {
15 CONST CHARSET = 'iso-8859-1';
16 CONST AUTH_APPROVED = 'approve';
17 CONST AUTH_DECLINED = 'decline';
18 CONST AUTH_BADDATA = 'baddata';
19 CONST AUTH_ERROR = 'error';
20
21 static protected $_mode = NULL;
22
23 static protected $_params = array();
24
25 /**
26 * We only need one instance of this object. So we use the singleton
27 * pattern and cache the instance in this variable
28 *
29 * @var object
30 * @static
31 */
32 static private $_singleton = NULL;
33
34 /**
35 * Constructor
36 *
37 * @param string $mode the mode of operation: live or test
38 *
39 * @return void
40 */ function __construct($mode, &$paymentProcessor) {
41 $this->_mode = $mode;
42
43 $this->_paymentProcessor = $paymentProcessor;
44
45 $this->_processorName = ts('TrustCommerce');
46
47 $config = CRM_Core_Config::singleton();
48 $this->_setParam('user_name', $paymentProcessor['user_name']);
49 $this->_setParam('password', $paymentProcessor['password']);
50
51 $this->_setParam('timestamp', time());
52 srand(time());
53 $this->_setParam('sequence', rand(1, 1000));
54 $this->logging_level = TRUSTCOMMERCE_LOGGING_LEVEL;
55 }
56
57 /**
58 * singleton function used to manage this object
59 *
60 * @param string $mode the mode of operation: live or test
61 *
62 * @return object
63 * @static
64 *
65 */
66 static
67 function &singleton($mode, &$paymentProcessor) {
68 $processorName = $paymentProcessor['name'];
69 if (self::$_singleton[$processorName] === NULL) {
70 self::$_singleton[$processorName] = new org_fsf_payment_trustcommerce($mode, $paymentProcessor);
71 }
72 return self::$_singleton[$processorName];
73 }
74
75 /**
76 * Submit a payment using Advanced Integration Method
77 *
78 * @param array $params assoc array of input parameters for this transaction
79 *
80 * @return array the result in a nice formatted array (or an error object)
81 * @public
82 */
83 function doDirectPayment(&$params) {
84 if (!extension_loaded("tclink")) {
85 return self::error(9001, 'TrustCommerce requires that the tclink module is loaded');
86 }
87
71a6ba5c
LMM
88 $newParams = $params;
89 if (CRM_Utils_Array::value('is_recur', $params) &&
90 $params['contributionRecurID']
91 ) {
92 CRM_Utils_Hook::alterPaymentProcessorParams($this,
93 $params,
94 $newParams
95 );
96 }
97 foreach ($newParams as $field => $value) {
98 $this->_setParam($field, $value);
99 }
100
101 if (CRM_Utils_Array::value('is_recur', $params) &&
102 $params['contributionRecurID']
103 ) {
104 return $this->doRecurPayment($params);
105 }
106
107 $postFields = array();
108 $tclink = $this->_getTrustCommerceFields();
109
110 // Set up our call for hook_civicrm_paymentProcessor,
111 // since we now have our parameters as assigned for the AIM back end.
112 CRM_Utils_Hook::alterPaymentProcessorParams($this,
113 $params,
114 $tclink
115 );
116
117 // TrustCommerce will not refuse duplicates, so we should check if the user already submitted this transaction
118 if ($this->_checkDupe($tclink['ticket'])) {
119 return self::error(9004, 'It appears that this transaction is a duplicate. Have you already submitted the form once? If so there may have been a connection problem. You can try your transaction again. If you continue to have problems please contact the site administrator.');
120 }
121
122 $result = tclink_send($tclink);
123
124 if (!$result) {
125 return self::error(9002, 'Could not initiate connection to payment gateway');
126 }
127
128 foreach ($result as $field => $value) {
129 error_log("result: $field => $value");
130 }
131
132 switch($result['status']) {
133 case self::AUTH_APPROVED:
134 // It's all good
135 break;
136 case self::AUTH_DECLINED:
137 // TODO FIXME be more or less specific?
138 // declinetype can be: decline, avs, cvv, call, expiredcard, carderror, authexpired, fraud, blacklist, velocity
139 // See TC documentation for more info
140 return self::error(9009, "Your transaction was declined: {$result['declinetype']}");
141 break;
142 case self::AUTH_BADDATA:
143 // TODO FIXME do something with $result['error'] and $result['offender']
144 return self::error(9011, "Invalid credit card information. Please re-enter.");
145 break;
146 case self::AUTH_ERROR:
147 return self::error(9002, 'Could not initiate connection to payment gateway');
148 break;
149 }
150
151 // Success
152
153 $params['trxn_id'] = $result['transid'];
154 $params['gross_amount'] = $tclink['amount'] / 100;
155
156 return $params;
157 }
158
159 /**
160 * Submit an Automated Recurring Billing subscription
161 *
162 * @param array $params assoc array of input parameters for this transaction
163 *
164 * @return array the result in a nice formatted array (or an error object)
165 * @public
166 */
167 function doRecurPayment(&$params) {
285eaf25
LMM
168 $payments = $this->_getParam('frequency_interval');
169 $cycle = $this->_getParam('frequency_unit');
71a6ba5c 170
285eaf25
LMM
171 /* Sort out our billing scheme */
172 switch($cycle) {
173 case 'day':
174 $cycle = 'd';
175 break;
176 case 'week':
177 $cycle = 'w';
178 break;
179 case 'month':
180 $cycle = 'm';
181 break;
182 case 'year':
183 $cycle = 'y';
184 break;
185 default:
186 return self::error(9001, 'Payment interval not set! Unable to process payment.');
187 break;
71a6ba5c
LMM
188 }
189
71a6ba5c 190
285eaf25
LMM
191 $params['authnow'] = 'y'; /* Process this payment `now' */
192 $params['cycle'] = $cycle; /* The billing cycle in years, months, weeks, or days. */
193 $params['payments'] = $payments;
71a6ba5c 194
71a6ba5c 195
285eaf25 196 $tclink = $this->_getTrustCommerceFields();
71a6ba5c 197
285eaf25
LMM
198 // Set up our call for hook_civicrm_paymentProcessor,
199 // since we now have our parameters as assigned for the AIM back end.
200 CRM_Utils_Hook::alterPaymentProcessorParams($this,
201 $params,
202 $tclink
203 );
71a6ba5c 204
285eaf25
LMM
205 // TrustCommerce will not refuse duplicates, so we should check if the user already submitted this transaction
206 if ($this->_checkDupe($tclink['ticket'])) {
207 return self::error(9004, 'It appears that this transaction is a duplicate. Have you already submitted the form once? If so there may have been a connection problem. You can try your transaction again. If you continue to have problems please contact the site administrator.');
71a6ba5c 208 }
71a6ba5c 209
285eaf25 210 $result = tclink_send($tclink);
71a6ba5c 211
a79ba043
LMM
212 $result = _getTrustCommereceResponse($result);
213
214 if($result == 0) {
215 /* Transaction was sucessful */
216 $params['trxn_id'] = $result['transid']; /* Get our transaction ID */
217 $params['gross_amount'] = $tclink['amount']/100; /* Convert from cents to dollars */
218 return $params;
219 } else {
220 /* Transaction was *not* successful */
221 return $result;
222 }
223 }
224
225 /* Parses a response from TC via the tclink_send() command.
226 * @param $reply array The result of a call to tclink_send().
227 * @return mixed self::error() if transaction failed, otherwise returns 0.
228 */
229 function _getTrustCommerceResponse($reply) {
71a6ba5c 230
285eaf25
LMM
231 /* DUPLIATE CODE, please refactor. ~lisa */
232 if (!$result) {
233 return self::error(9002, 'Could not initiate connection to payment gateway');
71a6ba5c
LMM
234 }
235
285eaf25
LMM
236 switch($result['status']) {
237 case self::AUTH_APPROVED:
238 // It's all good
239 break;
240 case self::AUTH_DECLINED:
241 // TODO FIXME be more or less specific?
242 // declinetype can be: decline, avs, cvv, call, expiredcard, carderror, authexpired, fraud, blacklist, velocity
243 // See TC documentation for more info
244 return self::error(9009, "Your transaction was declined: {$result['declinetype']}");
245 break;
246 case self::AUTH_BADDATA:
247 // TODO FIXME do something with $result['error'] and $result['offender']
248 return self::error(9011, "Invalid credit card information. Please re-enter.");
249 break;
250 case self::AUTH_ERROR:
251 return self::error(9002, 'Could not initiate connection to payment gateway');
252 break;
71a6ba5c 253 }
a79ba043 254 return 0;
71a6ba5c
LMM
255 }
256
257 function _getTrustCommerceFields() {
258 // Total amount is from the form contribution field
259 $amount = $this->_getParam('total_amount');
260 // CRM-9894 would this ever be the case??
261 if (empty($amount)) {
262 $amount = $this->_getParam('amount');
263 }
264 $fields = array();
265 $fields['custid'] = $this->_getParam('user_name');
266 $fields['password'] = $this->_getParam('password');
267 $fields['action'] = 'sale';
268
269 // Enable address verification
270 $fields['avs'] = 'y';
271
272 $fields['address1'] = $this->_getParam('street_address');
273 $fields['zip'] = $this->_getParam('postal_code');
274
275 $fields['name'] = $this->_getParam('billing_first_name') . ' ' . $this->_getParam('billing_last_name');
276
277 // This assumes currencies where the . is used as the decimal point, like USD
278 $amount = preg_replace("/([^0-9\\.])/i", "", $amount);
279
280 // We need to pass the amount to TrustCommerce in dollar cents
281 $fields['amount'] = $amount * 100;
282
283 // Unique identifier
284 $fields['ticket'] = substr($this->_getParam('invoiceID'), 0, 20);
285
286 // cc info
287 $fields['cc'] = $this->_getParam('credit_card_number');
288 $fields['cvv'] = $this->_getParam('cvv2');
289 $exp_month = str_pad($this->_getParam('month'), 2, '0', STR_PAD_LEFT);
290 $exp_year = substr($this->_getParam('year'),-2);
291 $fields['exp'] = "$exp_month$exp_year";
292
293 if ($this->_mode != 'live') {
294 $fields['demo'] = 'y';
295 }
71a6ba5c
LMM
296 return $fields;
297 }
298
299 /**
300 * Checks to see if invoice_id already exists in db
301 *
302 * @param int $invoiceId The ID to check
303 *
304 * @return bool True if ID exists, else false
305 */
306 function _checkDupe($invoiceId) {
307 require_once 'CRM/Contribute/DAO/Contribution.php';
308 $contribution = new CRM_Contribute_DAO_Contribution();
309 $contribution->invoice_id = $invoiceId;
310 return $contribution->find();
311 }
312
313 /**
314 * Get the value of a field if set
315 *
316 * @param string $field the field
317 *
318 * @return mixed value of the field, or empty string if the field is
319 * not set
320 */
321 function _getParam($field) {
322 return CRM_Utils_Array::value($field, $this->_params, '');
323 }
324
325 function &error($errorCode = NULL, $errorMessage = NULL) {
326 $e = CRM_Core_Error::singleton();
327 if ($errorCode) {
328 $e->push($errorCode, 0, NULL, $errorMessage);
329 }
330 else {
331 $e->push(9001, 0, NULL, 'Unknown System Error.');
332 }
333 return $e;
334 }
335
336 /**
337 * Set a field to the specified value. Value must be a scalar (int,
338 * float, string, or boolean)
339 *
340 * @param string $field
341 * @param mixed $value
342 *
343 * @return bool false if value is not a scalar, true if successful
344 */
345 function _setParam($field, $value) {
346 if (!is_scalar($value)) {
347 return FALSE;
348 }
349 else {
350 $this->_params[$field] = $value;
351 }
352 }
353
354 /**
355 * This function checks to see if we have the right config values
356 *
357 * @return string the error message if any
358 * @public
359 */
360 function checkConfig() {
361 $error = array();
362 if (empty($this->_paymentProcessor['user_name'])) {
363 $error[] = ts('Customer ID is not set for this payment processor');
364 }
365
366 if (empty($this->_paymentProcessor['password'])) {
367 $error[] = ts('Password is not set for this payment processor');
368 }
369
370 if (!empty($error)) {
371 return implode('<p>', $error);
372 } else {
373 return NULL;
374 }
375 }
376
377 function cancelSubscriptionURL($entityID = NULL, $entity = NULL) {
378 if ($entityID && $entity == 'membership') {
379 require_once 'CRM/Contact/BAO/Contact/Utils.php';
380 $contactID = CRM_Core_DAO::getFieldValue("CRM_Member_DAO_Membership", $entityID, "contact_id");
381 $checksumValue = CRM_Contact_BAO_Contact_Utils::generateChecksum($contactID, NULL, 'inf');
382
383 return CRM_Utils_System::url('civicrm/contribute/unsubscribe',
384 "reset=1&mid={$entityID}&cs={$checksumValue}", TRUE, NULL, FALSE, FALSE
385 );
386 }
387
388 return ($this->_mode == 'test') ? 'https://test.authorize.net' : 'https://authorize.net';
389 }
390
391 function cancelSubscription() {
392 $template = CRM_Core_Smarty::singleton();
393
394 $template->assign('subscriptionType', 'cancel');
395
396 $template->assign('apiLogin', $this->_getParam('apiLogin'));
397 $template->assign('paymentKey', $this->_getParam('paymentKey'));
398 $template->assign('subscriptionId', $this->_getParam('subscriptionId'));
399
400 $arbXML = $template->fetch('CRM/Contribute/Form/Contribution/AuthorizeNetARB.tpl');
401
402 // submit to authorize.net
403 $submit = curl_init($this->_paymentProcessor['url_recur']);
404 if (!$submit) {
405 return self::error(9002, 'Could not initiate connection to payment gateway');
406 }
407
408 curl_setopt($submit, CURLOPT_RETURNTRANSFER, 1);
409 curl_setopt($submit, CURLOPT_HTTPHEADER, array("Content-Type: text/xml"));
410 curl_setopt($submit, CURLOPT_HEADER, 1);
411 curl_setopt($submit, CURLOPT_POSTFIELDS, $arbXML);
412 curl_setopt($submit, CURLOPT_POST, 1);
413 curl_setopt($submit, CURLOPT_SSL_VERIFYPEER, 0);
414
415 $response = curl_exec($submit);
416
417 if (!$response) {
418 return self::error(curl_errno($submit), curl_error($submit));
419 }
420
421 curl_close($submit);
422
423 $responseFields = $this->_ParseArbReturn($response);
424
425 if ($responseFields['resultCode'] == 'Error') {
426 return self::error($responseFields['code'], $responseFields['text']);
427 }
428
429 // carry on cancelation procedure
430 return TRUE;
431 }
432
433 public function install() {
434 return TRUE;
435 }
436
437 public function uninstall() {
438 return TRUE;
439 }
440
441}