'linkpointapi',
'title' => t('Linkpoint API'),
'description' => t('Process credit card payments using the service of Linkpint API.'),
'settings' => 'uc_linkpoint_api_settings_form',
'credit' => 'uc_linkpoint_api_charge',
'credit_txn_types' => array(UC_CREDIT_AUTH_ONLY, UC_CREDIT_PRIOR_AUTH_CAPTURE, UC_CREDIT_AUTH_CAPTURE),
);
return $gateways;
}
/*******************************************************************************
* Callback Functions, Forms, and Tables
******************************************************************************/
/**
* Callback for payment gateway settings.
*/
function uc_linkpoint_api_settings_form() {
$form['linkpoint_api_settings'] = array(
'#type' => 'fieldset',
'#title' => t('Linkpoint API settings'),
);
$form['linkpoint_api_settings']['linkpoint_api_login_id'] = array(
'#type' => 'textfield',
'#title' => t('Store Number'),
'#default_value' => variable_get('linkpoint_api_login_id', ''),
'#description' => t("Enter your 10 digit Store Number assigned to you by Linkpoint."),
);
$form['linkpoint_api_settings']['linkpoint_api_transaction_key'] = array(
'#type' => 'textfield',
'#title' => t('PEM File'),
'#default_value' => variable_get('linkpoint_api_transaction_key', ''),
'#description' => t('Enter the absolute path of your PEM file. You must have this for Linkpoint API to work. Get this by logging in to Linkpoint Central > Support > Download Center'),
);
$form['linkpoint_api_settings']['linkpoint_api_transaction_mode'] = array(
'#type' => 'select',
'#title' => t('Transaction mode'),
'#description' => t('Transaction mode used for processing orders when on the LIVE server. You can use these options to test certain responses from the live server. If you need to do extensive testing you should create a test account at Linkpoint and select the Test server below.'),
'#options' => array(
'LIVE' => t('Production Mode'),
'GOOD' => t('Test Approved Response'),
'DECLINE' => t('Test Decline Response'),
'DUPLICATE' => t('Test Duplicate Response'),
),
'#default_value' => variable_get('linkpoint_api_transaction_mode', 'LIVE'),
);
$form['linkpoint_api_settings']['linkpoint_api_order_prefix'] = array(
'#type' => 'textfield',
'#title' => t('Order Prefix'),
'#default_value' => variable_get('linkpoint_api_order_prefix', ''),
'#description' => t('Enter a prefix to prepend to the ordernumber before sending to Linkpoint. Useful if you have multiple websites using the same merchant account, and need to pass in a unique order id to this store.'),
);
$form['linkpoint_api_settings']['linkpoint_api_transaction_server'] = array(
'#type' => 'select',
'#title' => t('Transaction server'),
'#description' => t('Unless you have set up a test account at Linkpoint
and are doing extensive testing this should be Live.'),
'#options' => array(
'live' => t('Live Server'),
'test' => t('Test Server'),
),
'#default_value' => variable_get('linkpoint_api_transaction_server', 'live'),
);
return $form;
}
/**
* Validate function for our settings. Keep in mind that the settings form above is combined with a larger form,
* So we need to tell that larger form about it in a form_alter (below)
**/
function uc_linkpoint_api_settings_form_validate($form, &$form_state) {
// Verify that the given filename and path exists and is accessible
if (!empty($form_state['values']['linkpoint_api_transaction_key']) && !is_file($form_state['values']['linkpoint_api_transaction_key'])) {
form_set_error('linkpoint_api_transaction_key', t('%dir is not a valid file or directory', array('%dir' => $form_state['values']['linkpoint_api_transaction_key'])));
}
}
/**
* Implementation of hook_form_alter().
* Let the payment gateway settings form know that we are adding another validate function
*/
function uc_linkpoint_api_form_alter(&$form, $form_state, $form_id) {
if ($form_id == 'uc_payment_gateways_form') {
$form['#validate'][] = 'uc_linkpoint_api_settings_form_validate';
}
}
/*******************************************************************************
* Main credit card charge function
******************************************************************************/
function uc_linkpoint_api_charge($order_id, $amount, $data) {
global $user;
if (!function_exists('curl_init')) {
drupal_set_message(t('The Linkpint API service requires curl. Please talk to your system administrator to get this configured.'));
return array('success' => FALSE);
}
// This handles the case where a user cancels their order on the checkout screen
if ($_SESSION['uc_linkpoint_order'] != $order_id) {
$_SESSION['uc_linkpoint_order'] = $order_id;
unset($_SESSION['uc_linkpoint_attempt']);
}
$order = uc_order_load($order_id);
// Generate XML and then send it over for the transaction to occur
$xml = "";
$xml .= _uc_linkpt_xml_billing($order);
$xml .= _uc_linkpt_xml_shipping($order);
$xml .= _uc_linkpt_xml_order_options($order, $data);
$xml .= _uc_linkpt_xml_merchant_info($order);
$xml .= _uc_linkpt_xml_credit_card($order);
$xml .= _uc_linkpt_xml_payment($order, $amount);
$xml .= _uc_linkpt_xml_transaction_details($order, $data);
$xml .= _uc_linkpt_xml_notes($order);
$xml .= "";
$xml = _uc_linkpt_xml_clean($xml);
// Send to First Data
$response = _uc_linkpt_curl($xml);
// There has been an error
if ($response === false) {
watchdog('uc_linkpoint_api', "cURL error for order #!ordernum: %error", array("!ordernum" => $order->order_num, "%error" => curl_error($ch)), WATCHDOG_ERROR);
$message = t('Could not connect to payment gateway. Please contact us and let us know. We apologize for the inconvenience.');
$result = array(
'success' => FALSE,
'comment' => $message,
'message' => $message,
'uid' => $user->uid,
);
return $result;
}
// No cURL error, parse the result so we can use it
$response = _uc_linkpt_parse_result($response);
if ($response['approved'] != 'APPROVED') {
$result = array(
'success' => FALSE,
'comment' => t('Credit card payment declined: !text', array('!text' => $response['error'])),
'message' => t('Credit card payment declined: !text', array('!text' => $response['error'])),
'uid' => $user->uid,
);
}
else {
$result = array(
'success' => TRUE,
'comment' => t('Credit card payment processed successfully.
Code: !code', array('!code' => $response['approvalcode'])),
'message' => t('Credit card payment processed successfully.
Code: !code', array('!code' => $response['approvalcode'])),
'uid' => $user->uid,
);
unset($_SESSION['uc_linkpoint_attempt']);
// Took these next lines from Authorize.net's implementation
// If this was an authorization only transaction...
if ($data['txn_type'] == UC_CREDIT_AUTH_ONLY) {
// Log the authorization to the order.
uc_credit_log_authorization($order->order_id, $response['order_num'], $amount);
}
elseif ($data['txn_type'] == UC_CREDIT_PRIOR_AUTH_CAPTURE) {
uc_credit_log_prior_auth_capture($order->order_id, $data['auth_id']);
}
// Don't log this as a payment money wasn't actually captured.
if (in_array($data['txn_type'], array(UC_CREDIT_AUTH_ONLY))) {
$result['log_payment'] = FALSE;
}
}
// Get human readable interpretations of the AVS and CVV codes
$avs_response = substr($response['avs'],0,3);
$cvv_response = substr($response['avs'],3,4);
$avs_text = _uc_linkpt_parse_avs($avs_response);
$cvv_text = _uc_linkpt_parse_cvv($cvv_response);
// Build log text and add to logs
$comment = "Linkpoint Transaction Attempt:
";
if (!empty($response['approved'])) $comment .= _uc_linkpt_txn_map($data['txn_type']) . ": " .$response['approved']."
";
if (!empty($response['message'])) $comment .= "Message: ".$response['message'];
if (!empty($response['approvalcode'])) $comment .= "Approval Code: ".$response['approvalcode']."
";
if (!empty($response['error'])) $comment .= "Error Code: ".$response['error']."
";
if (!empty($avs_response)) $comment .= "AVS: ".$avs_response." - ".$avs_text."
";
if (!empty($cvv_response)) $comment .= "CVV: ".$cvv_response." - ".$cvv_text;
uc_order_comment_save($order_id, $user->uid, $comment, 'admin');
return $result;
}
/*******************************************************************************
* Functions to build XML
******************************************************************************/
function _uc_linkpt_xml_billing($order) {
global $user;
$billing_country = _uc_linkpt_get_country_iso($order->billing_country);
$xml = '';
$xml .="";
if (!empty($user->uid)) $xml .="" . $user->uid . "";
$xml .="" . $order->billing_first_name. " " . $order->billing_last_name . "";
$xml .="" . $order->billing_company . "";
$xml .="" . $order->billing_street1 . "";
$xml .="" . $order->billing_street2 . "";
$xml .="" . $order->billing_street1 . "";
$xml .="" . $order->billing_city . "";
$xml .="" . uc_get_zone_code($order->billing_zone) . "";
$xml .="" . $order->billing_postal_code . "";
$xml .="" . $billing_country . "";
$xml .="" . $order->billing_phone ."";
$xml .="" . $order->primary_email . "";
$xml .="";
return $xml;
}
function _uc_linkpt_xml_shipping($order) {
$delivery_country = _uc_linkpt_get_country_iso($order->delivery_country);
$xml = '';
$xml .="";
$xml .="" . $order->delivery_first_name . " " . $order->delivery_last_name . "";
$xml .="" . $order->delivery_street1 . "";
$xml .="" . $order->delivery_street2 . "";
$xml .="" . $order->delivery_city . "";
$xml .="" . uc_get_zone_code($order->delivery_zone) . "";
$xml .="" . $order->delivery_postal_code . "";
$xml .="" . $delivery_country . "";
// $xml .="" . $order->delivery_phone .""; // breaks XML with First Data
// $xml .="" . $order->primary_email . ""; // this as well
$xml .="";
return $xml;
}
function _uc_linkpt_xml_credit_card($order) {
$xml = '';
$xml .="";
$xml .="" . $order->payment_details['cc_number'] . "";
$xml .="" . $order->payment_details['cc_exp_month'] . "";
$xml .="" . substr($order->payment_details['cc_exp_year'], 2, 2) . " ";
if (!empty($order->payment_details['cc_cvv'])) {
$xml .="" . $order->payment_details['cc_cvv'] . "";
$xml .="provided";
}
$xml .="";
return $xml;
}
function _uc_linkpt_xml_payment($order, $amount) {
// Calculate shipping, tax, and subtotal
$shipping = 0;
if (is_array($order->line_items)) {
foreach ($order->line_items as $item) {
if ($item['type'] == 'shipping') {
$shipping += $item['amount'];
}
}
}
$tax = 0;
if (module_exists('uc_taxes')) {
foreach (uc_taxes_calculate($order) as $tax_item) {
$tax += $tax_item->amount;
}
}
$subtotal = $order->order_total - $tax - $shipping;
$xml = '';
$xml .="";
if (!empty($subtotal)) $xml .="" . $subtotal . "";
if (!empty($tax)) $xml .="" . $tax . "";
if (!empty($shipping)) $xml .="" . $shipping . "";
$xml .="".$amount."";
$xml .="";
return $xml;
}
function _uc_linkpt_xml_transaction_details($order, $data) {
if ($data['txn_type'] == UC_CREDIT_PRIOR_AUTH_CAPTURE) {
$oid = $data['auth_id'];
} else {
// Order numbers must be unique in Linkpoint, so if someone attempted a transaction (then failed),
// then went back and changed details, we must alter the order ID to avoid an error from Linkpoint
$order_id = $order->order_id;
$_SESSION['uc_linkpoint_attempt'] = empty($_SESSION['uc_linkpoint_attempt']) ? 1 : ($_SESSION['uc_linkpoint_attempt'] + 1);
$oid = $_SESSION['uc_linkpoint_attempt'] > 1 ? "$order_id-{$_SESSION['uc_linkpoint_attempt']}" : $order_id;
$oid = variable_get('linkpoint_api_order_prefix', '').$oid;
}
$xml = '';
$xml .="";
$xml .="ECI";
$xml .="" . $oid . "";
$xml .="" . $order_id . "";
$xml .="" . ip_address() . "";
$xml .="";
return $xml;
}
function _uc_linkpt_xml_order_options($order, $data) {
$ordertype = _uc_linkpt_txn_map($data['txn_type']);
$xml = '';
$xml .="";
$xml .="" . variable_get('linkpoint_api_transaction_mode', 'LIVE') . "";
$xml .="" . $ordertype . "";
$xml .="";
return $xml;
}
function _uc_linkpt_xml_merchant_info($order) {
if (variable_get('linkpoint_api_transaction_server', 'live') == "test") {
$host = "staging.linkpt.net";
} else {
$host = "secure.linkpt.net";
}
$xml = '';
$xml .="";
$xml .="" . variable_get('linkpoint_api_login_id', '') . "";
$xml .="" . variable_get('linkpoint_api_transaction_key', '') . "";
$xml .="". $host ."";
$xml .="1129";
$xml .="";
return $xml;
}
function _uc_linkpt_xml_notes($order) {
$xml = '';
// Build description
$description = '';
if (is_array($order->products)) {
foreach ($order->products as $product) {
if (!empty($description)) {
$description .= ' // ';
}
$description .= $product->title .' x'. $product->qty;
if ($product->data['attributes']) {
foreach ($product->data['attributes'] as $attrname => $options) {
$description .= ', '. $attrname .': ';
foreach ($options as $option) {
$description .= $option.', ';
}
}
}
}
}
$description = substr($description, 0, 1023);
$xml .="";
$xml .="" . $description . "";
$xml .="";
return $xml;
}
/*******************************************************************************
* Helper functions to make everything above happen :)
******************************************************************************/
/**
* Initiate a cURL session and send over the pre-built XML to First Data for processing
**/
function _uc_linkpt_curl($xml) {
// Open the cURL session
$ch = curl_init();
// Set the cURL options
// Determine the posting URL
if (variable_get('linkpoint_api_transaction_server', 'live') == "test") {
curl_setopt($ch, CURLOPT_URL, UC_LINKPOINT_TEST_URL);
curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 0); // test server does not support
curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, 0); // these two params
} else {
curl_setopt($ch, CURLOPT_URL, UC_LINKPOINT_LIVE_URL);
}
curl_setopt($ch, CURLOPT_VERBOSE, 1); // Present verbose error output, to standard error
curl_setopt($ch, CURLOPT_POST, 1); // POST Request
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); // Data to send in the POST request
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // Puts output to a returned string
curl_setopt($ch, CURLOPT_SSLCERT, variable_get('linkpoint_api_transaction_key', '')); // The public cert key generated by First Data
// Execute the cURL session
$result = curl_exec($ch);
// Close cURL session
curl_close($ch);
return $result;
}
/**
* Parse result information from First Data
* There are some additional fields that First Data returns to us, but
* we just pull out the useful ones.
**/
function _uc_linkpt_parse_result($result) {
// Format the output from XML into an array
preg_match_all ("/<(.*?)>(.*?)\", $result, $outarr, PREG_SET_ORDER);
$n = 0;
while (isset($outarr[$n])) {
$retarr[$outarr[$n][1]] = strip_tags($outarr[$n][0]);
$n++;
}
$response = array(
'approved' => $retarr['r_approved'],
'error' => $retarr['r_error'],
'approvalcode' => $retarr['r_code'],
'avs' => $retarr['r_avs'],
'response_message' => $retarr['r_message'],
'order_num' => $retarr['r_ordernum'],
);
return $response;
}
/**
* Associate Ubercarts transaction types with First Data's
**/
function _uc_linkpt_txn_map($type) {
switch ($type) {
case UC_CREDIT_AUTH_ONLY:
$ordertype = "PREAUTH";
break;
case UC_CREDIT_AUTH_CAPTURE:
$ordertype = "SALE";
break;
case UC_CREDIT_PRIOR_AUTH_CAPTURE:
$ordertype = "POSTAUTH";
break;
}
return $ordertype;
}
/**
* Get the two character country code from the database
**/
function _uc_linkpt_get_country_iso($country) {
$countries = uc_get_country_data(
array(
'country_id' => $country,
)
);
return $countries[0]['country_iso_code_2'];
}
/**
* Clean up the XML
**/
function _uc_linkpt_xml_clean($xml) {
/* remove ampersand and appostrophes from text */
$xml = str_replace("&", "and", $xml);
$xml = str_replace("'", " ", $xml);
return $xml;
}
function _uc_linkpt_parse_avs($avs) {
$firsttwo = substr($avs, 0,2);
$third = substr($avs,2,3);
$parsed = '';
switch($third) {
case 'A':
$parsed = t('Address (Street) matches, ZIP does not');
if ($firsttwo == "YY") {
// discover
$parsed = t('Address (Street) and five digit ZIP match');
}
break;
case 'B':
$parsed = t('Address information not provided for AVS check');
break;
case 'E':
$parsed = t('AVS error');
break;
case 'G':
$parsed = t('Non-U.S. Card Issuing Bank');
break;
case 'N':
$parsed = t('No Match on Address (Street) or ZIP');
break;
case 'P':
$parsed = t('AVS not applicable for this transaction');
break;
case 'R':
$parsed = t('Retry Ð System unavailable or timed out');
break;
case 'S':
$parsed = t('Service not supported by issuer');
break;
case 'U':
$parsed = t('Address information is unavailable');
break;
case 'X':
$parsed = t('Address (Street) and nine digit ZIP match');
break;
case 'Y':
$parsed = t('Address (Street) and five digit ZIP match');
if ($firsttwo == "YN") {
// discover
$parsed = t('Address (Street) matches, ZIP does not');
}
break;
case 'Z':
$parsed = t('Five digit ZIP matches, Address (Street) does not');
break;
default:
$parsed = t('Unknown');
}
return $parsed;
}
function _uc_linkpt_parse_cvv($code) {
$parsed = '';
switch ($code) {
case 'M':
$parsed = t('Match');
break;
case 'N':
$parsed = t('No Match');
break;
case 'P':
$parsed = t('Not Processed');
break;
case 'S':
$parsed = t('Should have been present');
break;
case 'U':
$parsed = t('Issuer unable to process request');
break;
default:
$parsed = t('Unkown');
}
return $parsed;
}