(http://www.fsf.org) * */ // Define logging level (0 = off, 4 = log everything) define('TRUSTCOMMERCE_LOGGING_LEVEL', 4); require_once 'CRM/Core/Payment.php'; class org_fsf_payment_trustcommerce extends CRM_Core_Payment { CONST CHARSET = 'iso-8859-1'; CONST AUTH_APPROVED = 'approve'; CONST AUTH_DECLINED = 'decline'; CONST AUTH_BADDATA = 'baddata'; CONST AUTH_ERROR = 'error'; static protected $_mode = NULL; static protected $_params = array(); /** * We only need one instance of this object. So we use the singleton * pattern and cache the instance in this variable * * @var object * @static */ static private $_singleton = NULL; /** * Constructor * * @param string $mode the mode of operation: live or test * * @return void */ function __construct($mode, &$paymentProcessor) { $this->_mode = $mode; $this->_paymentProcessor = $paymentProcessor; $this->_processorName = ts('TrustCommerce'); $config = CRM_Core_Config::singleton(); $this->_setParam('user_name', $paymentProcessor['user_name']); $this->_setParam('password', $paymentProcessor['password']); $this->_setParam('timestamp', time()); srand(time()); $this->_setParam('sequence', rand(1, 1000)); $this->logging_level = TRUSTCOMMERCE_LOGGING_LEVEL; } /** * singleton function used to manage this object * * @param string $mode the mode of operation: live or test * * @return object * @static * */ static function &singleton($mode, &$paymentProcessor) { $processorName = $paymentProcessor['name']; if (self::$_singleton[$processorName] === NULL) { self::$_singleton[$processorName] = new org_fsf_payment_trustcommerce($mode, $paymentProcessor); } return self::$_singleton[$processorName]; } /** * Submit a payment using Advanced Integration Method * * @param array $params assoc array of input parameters for this transaction * * @return array the result in a nice formatted array (or an error object) * @public */ function doDirectPayment(&$params) { if (!extension_loaded("tclink")) { return self::error(9001, 'TrustCommerce requires that the tclink module is loaded'); } /* * recurpayment function does not compile an array & then proces it - * - the tpl does the transformation so adding call to hook here * & giving it a change to act on the params array */ $newParams = $params; if (CRM_Utils_Array::value('is_recur', $params) && $params['contributionRecurID'] ) { CRM_Utils_Hook::alterPaymentProcessorParams($this, $params, $newParams ); } foreach ($newParams as $field => $value) { $this->_setParam($field, $value); } if (CRM_Utils_Array::value('is_recur', $params) && $params['contributionRecurID'] ) { return $this->doRecurPayment($params); } $postFields = array(); $tclink = $this->_getTrustCommerceFields(); // Set up our call for hook_civicrm_paymentProcessor, // since we now have our parameters as assigned for the AIM back end. CRM_Utils_Hook::alterPaymentProcessorParams($this, $params, $tclink ); // TrustCommerce will not refuse duplicates, so we should check if the user already submitted this transaction if ($this->_checkDupe($tclink['ticket'])) { 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.'); } $result = tclink_send($tclink); if (!$result) { return self::error(9002, 'Could not initiate connection to payment gateway'); } foreach ($result as $field => $value) { error_log("result: $field => $value"); } switch($result['status']) { case self::AUTH_APPROVED: // It's all good break; case self::AUTH_DECLINED: // TODO FIXME be more or less specific? // declinetype can be: decline, avs, cvv, call, expiredcard, carderror, authexpired, fraud, blacklist, velocity // See TC documentation for more info return self::error(9009, "Your transaction was declined: {$result['declinetype']}"); break; case self::AUTH_BADDATA: // TODO FIXME do something with $result['error'] and $result['offender'] return self::error(9011, "Invalid credit card information. Please re-enter."); break; case self::AUTH_ERROR: return self::error(9002, 'Could not initiate connection to payment gateway'); break; } // Success $params['trxn_id'] = $result['transid']; $params['gross_amount'] = $tclink['amount'] / 100; return $params; } /** * Submit an Automated Recurring Billing subscription * * @param array $params assoc array of input parameters for this transaction * * @return array the result in a nice formatted array (or an error object) * @public */ function doRecurPayment(&$params) { $template = CRM_Core_Smarty::singleton(); $intervalLength = $this->_getParam('frequency_interval'); $intervalUnit = $this->_getParam('frequency_unit'); if ($intervalUnit == 'week') { $intervalLength *= 7; $intervalUnit = 'days'; } elseif ($intervalUnit == 'year') { $intervalLength *= 12; $intervalUnit = 'months'; } elseif ($intervalUnit == 'day') { $intervalUnit = 'days'; } elseif ($intervalUnit == 'month') { $intervalUnit = 'months'; } // interval cannot be less than 7 days or more than 1 year if ($intervalUnit == 'days') { if ($intervalLength < 7) { return self::error(9001, 'Payment interval must be at least one week'); } elseif ($intervalLength > 365) { return self::error(9001, 'Payment interval may not be longer than one year'); } } elseif ($intervalUnit == 'months') { if ($intervalLength < 1) { return self::error(9001, 'Payment interval must be at least one week'); } elseif ($intervalLength > 12) { return self::error(9001, 'Payment interval may not be longer than one year'); } } $template->assign('intervalLength', $intervalLength); $template->assign('intervalUnit', $intervalUnit); $template->assign('apiLogin', $this->_getParam('apiLogin')); $template->assign('paymentKey', $this->_getParam('paymentKey')); $template->assign('refId', substr($this->_getParam('invoiceID'), 0, 20)); //for recurring, carry first contribution id $template->assign('invoiceNumber', $this->_getParam('contributionID')); $firstPaymentDate = $this->_getParam('receive_date'); if (!empty($firstPaymentDate)) { //allow for post dated payment if set in form $template->assign('startDate', date('Y-m-d', strtotime($firstPaymentDate))); } else { $template->assign('startDate', date('Y-m-d')); } // for open ended subscription totalOccurrences has to be 9999 $installments = $this->_getParam('installments'); $template->assign('totalOccurrences', $installments ? $installments : 9999); $template->assign('amount', $this->_getParam('amount')); $template->assign('cardNumber', $this->_getParam('credit_card_number')); $exp_month = str_pad($this->_getParam('month'), 2, '0', STR_PAD_LEFT); $exp_year = $this->_getParam('year'); $template->assign('expirationDate', $exp_year . '-' . $exp_month); // name rather than description is used in the tpl - see http://www.authorize.net/support/ARB_guide.pdf $template->assign('name', $this->_getParam('description')); $template->assign('email', $this->_getParam('email')); $template->assign('contactID', $this->_getParam('contactID')); $template->assign('billingFirstName', $this->_getParam('billing_first_name')); $template->assign('billingLastName', $this->_getParam('billing_last_name')); $template->assign('billingAddress', $this->_getParam('street_address')); $template->assign('billingCity', $this->_getParam('city')); $template->assign('billingState', $this->_getParam('state_province')); $template->assign('billingZip', $this->_getParam('postal_code')); $template->assign('billingCountry', $this->_getParam('country')); $arbXML = $template->fetch('CRM/Contribute/Form/Contribution/AuthorizeNetARB.tpl'); // submit to authorize.net $submit = curl_init($this->_paymentProcessor['url_recur']); if (!$submit) { return self::error(9002, 'Could not initiate connection to payment gateway'); } curl_setopt($submit, CURLOPT_RETURNTRANSFER, 1); curl_setopt($submit, CURLOPT_HTTPHEADER, array("Content-Type: text/xml")); curl_setopt($submit, CURLOPT_HEADER, 1); curl_setopt($submit, CURLOPT_POSTFIELDS, $arbXML); curl_setopt($submit, CURLOPT_POST, 1); curl_setopt($submit, CURLOPT_SSL_VERIFYPEER, 0); $response = curl_exec($submit); if (!$response) { return self::error(curl_errno($submit), curl_error($submit)); } curl_close($submit); $responseFields = $this->_ParseArbReturn($response); if ($responseFields['resultCode'] == 'Error') { return self::error($responseFields['code'], $responseFields['text']); } // update recur processor_id with subscriptionId CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_ContributionRecur', $params['contributionRecurID'], 'processor_id', $responseFields['subscriptionId'] ); //only impact of assigning this here is is can be used to cancel the subscription in an automated test // if it isn't cancelled a duplicate transaction error occurs if (CRM_Utils_Array::value('subscriptionId', $responseFields)) { $this->_setParam('subscriptionId', $responseFields['subscriptionId']); } return $params; } function _getTrustCommerceFields() { // Total amount is from the form contribution field $amount = $this->_getParam('total_amount'); // CRM-9894 would this ever be the case?? if (empty($amount)) { $amount = $this->_getParam('amount'); } $fields = array(); $fields['custid'] = $this->_getParam('user_name'); $fields['password'] = $this->_getParam('password'); $fields['action'] = 'sale'; // Enable address verification $fields['avs'] = 'y'; $fields['address1'] = $this->_getParam('street_address'); $fields['zip'] = $this->_getParam('postal_code'); $fields['name'] = $this->_getParam('billing_first_name') . ' ' . $this->_getParam('billing_last_name'); // This assumes currencies where the . is used as the decimal point, like USD $amount = preg_replace("/([^0-9\\.])/i", "", $amount); // We need to pass the amount to TrustCommerce in dollar cents $fields['amount'] = $amount * 100; // Unique identifier $fields['ticket'] = substr($this->_getParam('invoiceID'), 0, 20); // cc info $fields['cc'] = $this->_getParam('credit_card_number'); $fields['cvv'] = $this->_getParam('cvv2'); $exp_month = str_pad($this->_getParam('month'), 2, '0', STR_PAD_LEFT); $exp_year = substr($this->_getParam('year'),-2); $fields['exp'] = "$exp_month$exp_year"; if ($this->_mode != 'live') { $fields['demo'] = 'y'; } // TODO FIXME remove foreach ($fields as $field => $value) { if ($field == 'custid') $value = '********'; if ($field == 'password') $value = '********'; if ($field == 'cc') $value = '********'; if ($field == 'cvv') $value = '********'; if ($field == 'exp') $value = '********'; error_log("fields: $field => $value"); } return $fields; } /** * Checks to see if invoice_id already exists in db * * @param int $invoiceId The ID to check * * @return bool True if ID exists, else false */ function _checkDupe($invoiceId) { require_once 'CRM/Contribute/DAO/Contribution.php'; $contribution = new CRM_Contribute_DAO_Contribution(); $contribution->invoice_id = $invoiceId; return $contribution->find(); } /** * Get the value of a field if set * * @param string $field the field * * @return mixed value of the field, or empty string if the field is * not set */ function _getParam($field) { return CRM_Utils_Array::value($field, $this->_params, ''); } function &error($errorCode = NULL, $errorMessage = NULL) { $e = CRM_Core_Error::singleton(); if ($errorCode) { $e->push($errorCode, 0, NULL, $errorMessage); } else { $e->push(9001, 0, NULL, 'Unknown System Error.'); } return $e; } /** * Set a field to the specified value. Value must be a scalar (int, * float, string, or boolean) * * @param string $field * @param mixed $value * * @return bool false if value is not a scalar, true if successful */ function _setParam($field, $value) { if (!is_scalar($value)) { return FALSE; } else { $this->_params[$field] = $value; } } /** * This function checks to see if we have the right config values * * @return string the error message if any * @public */ function checkConfig() { $error = array(); if (empty($this->_paymentProcessor['user_name'])) { $error[] = ts('Customer ID is not set for this payment processor'); } if (empty($this->_paymentProcessor['password'])) { $error[] = ts('Password is not set for this payment processor'); } if (!empty($error)) { return implode('

', $error); } else { return NULL; } } function cancelSubscriptionURL($entityID = NULL, $entity = NULL) { if ($entityID && $entity == 'membership') { require_once 'CRM/Contact/BAO/Contact/Utils.php'; $contactID = CRM_Core_DAO::getFieldValue("CRM_Member_DAO_Membership", $entityID, "contact_id"); $checksumValue = CRM_Contact_BAO_Contact_Utils::generateChecksum($contactID, NULL, 'inf'); return CRM_Utils_System::url('civicrm/contribute/unsubscribe', "reset=1&mid={$entityID}&cs={$checksumValue}", TRUE, NULL, FALSE, FALSE ); } return ($this->_mode == 'test') ? 'https://test.authorize.net' : 'https://authorize.net'; } function cancelSubscription() { $template = CRM_Core_Smarty::singleton(); $template->assign('subscriptionType', 'cancel'); $template->assign('apiLogin', $this->_getParam('apiLogin')); $template->assign('paymentKey', $this->_getParam('paymentKey')); $template->assign('subscriptionId', $this->_getParam('subscriptionId')); $arbXML = $template->fetch('CRM/Contribute/Form/Contribution/AuthorizeNetARB.tpl'); // submit to authorize.net $submit = curl_init($this->_paymentProcessor['url_recur']); if (!$submit) { return self::error(9002, 'Could not initiate connection to payment gateway'); } curl_setopt($submit, CURLOPT_RETURNTRANSFER, 1); curl_setopt($submit, CURLOPT_HTTPHEADER, array("Content-Type: text/xml")); curl_setopt($submit, CURLOPT_HEADER, 1); curl_setopt($submit, CURLOPT_POSTFIELDS, $arbXML); curl_setopt($submit, CURLOPT_POST, 1); curl_setopt($submit, CURLOPT_SSL_VERIFYPEER, 0); $response = curl_exec($submit); if (!$response) { return self::error(curl_errno($submit), curl_error($submit)); } curl_close($submit); $responseFields = $this->_ParseArbReturn($response); if ($responseFields['resultCode'] == 'Error') { return self::error($responseFields['code'], $responseFields['text']); } // carry on cancelation procedure return TRUE; } public function install() { return TRUE; } public function uninstall() { return TRUE; } }