Self validating order numbers

This post was partly derived from my earlier post on profiles for a simple CRM. If you’re thinking about creating your own online store and don’t want to use an off-the-shelf solution, you’ll eventually come across an apparently simple problem made complex by human nature.

How to generate an order number that :

  1. Doesn’t give away how many orders were placed so far.
  2. Isn’t too long as to be cumbersome nor too short as to repeat itself. Or at least avoid repeating for the same day.
  3. You can reasonably be sure is correct before searching.

The first problem can be made simpler by randomly generating the order number at all times. The second problem can be solved by adding a date and limiting the length of the “code” part to just ten characters. Since phone numbers in the U.S. are already ten numbers including the area code, this (I think) is a reasonable length. The only extra data is just a date.

So potentially, your order number will look like : 2010-07-17-63477-7CKFK

But how do you check whether this is a valid order number?

The first part would be generating that order number in the first place. You’ll need to create a random string that doesn’t contain potentially confusing characters like i, l, o, 1 etc… So let’s avoid lowercase letters and the first and last digit altogether, but let’s also not forget that people will inevitably enter in the order number in lowercase.

Your character pool should be as follows : 23456789ABCDEFGHKMNPQRSTUVWXYZ.
Note 1, 0 and letters “I”, “J”, “L” and “O” are missing.

Now we create a function to generate the random string from our character pool….

public static string RandomString(int lgt)
{
	string _range= "23456789ABCDEFGHKMNPQRSTUVWXYZ";

	Byte[] _bytes = new Byte[lgt];
	char[] _chars = new char[lgt];

	RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();

	rng.GetBytes(_bytes);

	for (int i = 0; i < lgt; i++)
		_chars[i] = _range[_bytes[i] % _range.Length];

	return new string(_chars);
}

The second step is creating an order number that includes some form of checksum that references itself. We can do this by making only the last five characters the actual order number and the middle five the checksum using the date and order number as references.

public static string NewCodeKey()
{
	string dt = string.Format("{0:yyyy-MM-dd}", DateTime.Now);
	string r = RandomString(5);

	// We use our date and the above random string and date to generate an 
	// MD5 hash

	byte[] b = Encoding.ASCII.GetBytes(dt + r);
	StringBuilder ck = new StringBuilder();

	MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
	b = md5.ComputeHash(b);

	for (int i = 0; i < b.Length; i++)
		ck.Append(b[i].ToString("x2").ToUpper());

	// Replace those pesky confusing letters and numbers and take the 
	// first 5 characters
	string c = ck.ToString()
		.Replace("0", "")
		.Replace("1", "")
		.Replace("I", "")
		.Replace("J", "")
		.Replace("L", "")
		.Replace("O", "")
		.Substring(0, 5);

	return dt + "-" + c + "-" + r;
}

So as in the above example order number, the self check of the date, 2010-07-17 and the actual order number, 7CKFK will equal 63477. But how do we check this? Every dash needs to be seperated and plugged into the system. Create the verifying function…

public static bool CheckCodeKey(string key)
{
	// No need to check an empty key
	if (string.IsNullOrEmpty(key))
		return false;

	// No dashes means we can't check this key anyway
	if (key.IndexOf('-') < 0)
		return false;

	key = key.ToUpper();

	string[] k = key.Split('-');

	// Every single dash is needed. The order number needs to be split into 
	// 5 parts.
	if (k.Length < 5)
		return false;

	// Every length has to be just right. Year is 4, month is 2, day is 2, 
	// checksum is 5 and order number is 5.
	if (k[0].Length < 4 || k[1].Length < 2 || k[2].Length < 2 || k[3].Length < 5 || k[4].Length < 5)
		return false;

	string dt = k[0] + "-" + k[1] + "-" + k[2];

	byte[] b = Encoding.ASCII.GetBytes(dt + k[4]);

	MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
	b = md5.ComputeHash(b);

	StringBuilder ck = new StringBuilder();
	for (int i = 0; i < b.Length; i++)
		ck.Append(b[i].ToString("x2").ToUpper());

	string ckv = ck.ToString()
		.Replace("0", "")
		.Replace("1", "")
		.Replace("I", "")
		.Replace("J", "")
		.Replace("L", "")
		.Replace("O", "")
		.Substring(0, 5);

	// Match the first 5 characters of the MD5 to the given checksum
	if (ckv == k[3])
		return true;

	return false;
}

Of course this doesn’t make it impossible to have order number collisions, just make them extremely unlikely. So you’ll still need to do a check, but will reasonably eliminate user errors. If even a single digit is off, the entire order number should be rejected as the MD5 hash takes into account not just the number, but the date as well. And of course, the checksum itself adds to reduce order number collisions.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s