UPDATE 2: The final code for this project is available for download on the Free Code page.
UPDATE: The code for this posting was updated 2-19-2008. I removed the
x_password reference. It is not listed in the current documentation, and according to Authorize.Net, the request needs to include the
API Login ID and the
TransactionCode instead. I also changed
isTestMode to
IsTestMode for consistency. I also confirmed with them that I need to submit the request from an SSL page, so I am still waiting for that before I can test the code.
My company has an account with
Authorize.net, and I am in the process of using it to replace our old PayPal system for online Credit Card purchases. We’ve used their Virtual Terminal system for several years, and once long ago I integrated their CC processing with a PHP site.
I guess it has to do with supplying a ubiquitous solution, but I find that their developer integration tools are somewhat lacking. They have several different methods of submitting and processing payments, and I believe we used SIM (Server Integration Method) previously. In this solution, also my first official foray into the world of ASP.NET, we will be using AIM (Advanced Integration Method) which gives us a much finer level of control over the user experience.
There is a developer’s guide and sample code, but I found the sample code to be lacking in anything terribly useful, like a “sample”. It does not really show you how to send the request or handle the response. I assumed correctly that I am not the first person who wanted a better example, so I hit the Google pavement and found this little gem from 2004 at The Jackol’s Den.
This code gave me a great start, but in looking at it I decided I wanted something a little more flexible. This sample shows all the code in a single method which I assume is supposed to be embedded in an ASP.NET page. I would like to have something more reusable, so I restructured the code a bit.
First, there were a number of items hard coded, such as the Authorize.net version and account information. I could see where you might want this information to be stored in a database or configuration file somewhere, so I put the account information a separate class.
public class AuthNetAccountInfo
{
public string AuthNetVersion {get; set;}
public string AuthNetLoginID { get; set; }
// public string AuthNetPassword { get; set; }
public string AuthNetTransKey { get; set; }
public bool IsTestMode { get; set; }
}
Second, some of the actual charge information was hard coded while the rest were passed in to the method as parameters. One item that was hard coded was the description, meaning that this approach would only be good for a single item (either that or the description had to be overly generic). Another is the card number and expiration date! Surely anyone would recognize that this could not go to production, but making it ready for prime time would again mean tying this method to a single page. Parameters such as first name, last name, address, and amount are passed in making the signature of the method long and a little unwieldy. So I created a TransactionRequestInfo class to handle all of these details.
public class TransactionRequestInfo
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Country { get; set; }
public string Description { get; set; }
private decimal _amount;
public decimal ChargeAmount
{
get
{
return _amount;
}
set
{
_amount = Decimal.Round(value, 2);
}
}
private string _zip;
public string Zip
{
get
{
return _zip;
}
set
{
int res;
if (int.TryParse(value, out res))
{
if (res > 99999)
{
throw new ArgumentException("Zip Code Value invalid");
}
else
{
_zip = res.ToString().PadLeft(5, '0');
}
}
else
{
throw new ArgumentException("Zip code must be numeric");
}
}
}
private string _securityCode;
public string SecurityCode
{
get
{
return _securityCode;
}
set
{
int res;
if (int.TryParse(value, out res))
{
if (res > 999)
{
throw new ArgumentException("Security Code Value invalid");
}
else
{
_securityCode = res.ToString().PadLeft(3, '0');
}
}
else
{
throw new ArgumentException("Security code must be numeric");
}
}
}
private string _cardNumber;
public string CardNumber
{
get
{
return _cardNumber;
}
set
{
long res;
if (long.TryParse(value, out res))
{
_cardNumber = res.ToString();
}
else
{
throw new ArgumentException("Card Number may only contain numbers");
}
}
}
private string _expDate;
public string ExpDate
{
get
{
return _expDate;
}
set
{
int res;
if (int.TryParse(value, out res))
{
string exp = res.ToString().PadLeft(4, '0');
int month = int.Parse(exp.Substring(0, 2));
int yr = int.Parse(exp.Substring(2, 2));
if (yr > DateTime.Now.Year ||
(yr == DateTime.Now.Year && month >= DateTime.Now.Month))
{
_expDate = month.ToString().PadLeft(2, '0') +
"/" + yr.ToString().PadLeft(2, '0');
}
else
{
throw new ArgumentException("Expiration Date already passed");
}
}
else
{
throw new ArgumentException("Zip code must be numeric");
}
}
}
}
Finally, the original method returned a Boolean indicating whether or not the charge was successful. It does a lot of work deciphering and interpreting the errors and building good error messages, but it writes the message to a Label control. Again, this code is intended for a single page. I felt that we needed a real response mechanism, so I created another class for that as well.
public class TransactionResponseInfo
{
public string AuthorizationCode { get; set; }
public string TransactionID { get; set; }
public string ReturnCode { get; set; }
public string Message { get; set; }
}
Finally, I made a Transaction class to hold the method and made the method static. As you’ll see, most of the original code remains intact, with the necessary changes to incorporate the new class structure discussed above.
public static class Transaction
{
public static TransactionResponseInfo ProcessPayment(
TransactionRequestInfo transaction, AuthNetAccountInfo account)
{
TransactionResponseInfo response = new TransactionResponseInfo();
WebClient objRequest = new WebClient();
System.Collections.Specialized.NameValueCollection objInf =
new System.Collections.Specialized.NameValueCollection(30);
//System.Collections.Specialized.NameValueCollection objRetInf =
// new System.Collections.Specialized.NameValueCollection(30);
byte[] objRetBytes;
string[] objRetVals;
string strError;
#region Set Request Values
objInf.Add("x_version", account.AuthNetVersion);
objInf.Add("x_delim_data", "True");
objInf.Add("x_login", account.AuthNetLoginID);
// objInf.Add("x_password", account.AuthNetPassword);
objInf.Add("x_tran_key", account.AuthNetTransKey);
objInf.Add("x_relay_response", "False");
objInf.Add("x_delim_char", ",");
objInf.Add("x_encap_char", "|");
// Billing Address
objInf.Add("x_first_name", transaction.FirstName);
objInf.Add("x_last_name", transaction.LastName);
objInf.Add("x_address", transaction.Address);
objInf.Add("x_city", transaction.City);
objInf.Add("x_state", transaction.State);
objInf.Add("x_zip", transaction.Zip);
objInf.Add("x_country", transaction.Country);
// Card Details
objInf.Add("x_card_num", transaction.CardNumber);
objInf.Add("x_exp_date", transaction.ExpDate);
// Authorization code of the card (CCV)
objInf.Add("x_card_code", transaction.SecurityCode);
objInf.Add("x_method", "CC");
objInf.Add("x_type", "AUTH_CAPTURE");
objInf.Add("x_amount", transaction.ChargeAmount.ToString());
objInf.Add("x_description", transaction.Description);
// Currency setting. Check the guide for other supported currencies
objInf.Add("x_currency_code", "USD");
if (account.IsTestMode)
{
// Pure Test Server
objInf.Add("x_test_request", "True");
objRequest.BaseAddress =
"https://test.authorize.net/gateway/transact.dll";
}
else if (!account.IsTestMode)
{
// Actual Server
objInf.Add("x_test_request", "False");
objRequest.BaseAddress =
"https://secure.authorize.net/gateway/transact.dll";
}
else
{
throw new Exception("Transaction Mode Invalid");
}
#endregion
try
{
// POST request
objRetBytes =
objRequest.UploadValues(objRequest.BaseAddress, "POST", objInf);
objRetVals =
System.Text.Encoding.ASCII.GetString(objRetBytes).Split(",".ToCharArray());
// Process Return Values
response.ReturnCode = objRetVals[0].Trim(char.Parse("|"));
if (objRetVals[0].Trim(char.Parse("|")) == "1")
{
// Returned Authorisation Code
response.AuthorizationCode = objRetVals[4].Trim(char.Parse("|"));
// Returned Transaction ID
response.TransactionID = objRetVals[6].Trim(char.Parse("|"));
strError = "Transaction completed successfully.";
}
else
{
// Error!
strError = objRetVals[3].Trim(char.Parse("|")) + " (" +
objRetVals[2].Trim(char.Parse("|")) + ")";
if (objRetVals[2].Trim(char.Parse("|")) == "44")
{
// CCV transaction decline
strError += "Our Card Code Verification (CCV) returned " +
"the following error: ";
switch (objRetVals[38].Trim(char.Parse("|")))
{
case "N":
strError += "Card Code does not match.";
break;
case "P":
strError += "Card Code was not processed.";
break;
case "S":
strError += "Card Code should be on card but was not indicated.";
break;
case "U":
strError += "Issuer was not certified for Card Code.";
break;
}
}
if (objRetVals[2].Trim(char.Parse("|")) == "45")
{
if (strError.Length > 1)
strError += "n";
// AVS transaction decline
strError += "Our Address Verification System (AVS) " +
"returned the following error: ";
switch (objRetVals[5].Trim(char.Parse("|")))
{
case "A":
strError += " the zip code entered does not match " +
"the billing address.";
break;
case "B":
strError += " no information was provided for the AVS check.";
break;
case "E":
strError += " a general error occurred in the AVS system.";
break;
case "G":
strError += " the credit card was issued by a non-US bank.";
break;
case "N":
strError += " neither the entered street address nor zip " +
"code matches the billing address.";
break;
case "P":
strError += " AVS is not applicable for this transaction.";
break;
case "R":
strError += " please retry the transaction; the AVS system " +
"was unavailable or timed out.";
break;
case "S":
strError += " the AVS service is not supported by your " +
"credit card issuer.";
break;
case "U":
strError += " address information is unavailable for the " +
"credit card.";
break;
case "W":
strError += " the 9 digit zip code matches, but the " +
"street address does not.";
break;
case "Z":
strError += " the zip code matches, but the address does not.";
break;
}
}
}
}
catch (Exception ex)
{
strError = ex.Message;
}
response.Message = strError;
return response;
}
}
One of the changes I made was the Test Mode handling. Previously, this was also hard coded, requiring the developer to uncomment certain code to make the service “live”. I felt it would be a good enhancement to make this code driven. This way, the test vs. live status can be switched on and off by the end user (or via configuration). The code uses System.Collections.Specialized.NameValueCollection objects extensively, which I wanted to upgrade to a generic Dictionary<string, string>, but the System.Net.WebClient methods required the old NameValueCollection.
I’m not ready to actually publish this code yet: I haven’t written the code to implement this and it still needs to be tested. If I understand AIM, the request will need to come from a server that has an SSL Certificate installed, which as of yet I do not have. {UPDATE: I rechecked the documentation and it says if you have SSL you “may” use AIM, so perhaps this is optional}
Once I have it all working, I will add it to the Free Code section. In the meantime, feel free to try what is here and let me know your thoughts.