CarlRaymond / jquery.cardswipe

jQuery plugin for magnetic card readers
MIT License
115 stars 49 forks source link

AAMVA (Driver's License) Parsing #19

Open rickjedi opened 8 years ago

rickjedi commented 8 years ago

I cannot seem to get the parser to recognize the AAMVA format. There are other plugins that do that, but I'm already using your plugin to parse payment card data so I need a modification that will parse driver's licenses.

Here's the configuration I'm using:

$.cardswipe({
        firstLineOnly: false,
        success: cardSwipeComplete,
        parsers: [aamva, "generic"],
        debug: true,
        error: errorCallback,
        prefixCharacter: '%',
    });

I write to the console whenever any one of the states is entered, but they never fire. In debug mode I get into the PENDING state, since I've changed the prefix from the standard "%B" to simply "%", but success, start, end, failure, none of those states gets entered.

I have not yet written the aamva parser, so I was hoping you could help. Here's the GIT that another developer published for that purpose: https://github.com/winfinit/aamvajs

Here's the standard for AAMVA (its actually in a table, but all the fields are there in three tracks):

AAMVA Driver’s License Format

TRACK 1 Field ID Character Contents Length* a Start Sentinel (%) 1 b State or Province 2 c City 13 d Name 35 e Address 29 f End Sentinel (?) 1 g Linear Redundancy Check (LRC) Character 1

TRACK 2 Field ID Characte r Contents Length a Start Sentinel (;) 1 b ANSI User Code 1 c ANSI User ID 5 d Jurisdiction ID/DL 14 e Expiration date 4 f Birthdate 8 g Remainder of Jurisdiction ID/DL 5 h End Sentinel (?) 1 i Linear Redundancy Check (LRC) Character 1

TRACK 3 Field ID Character Contents Length a Start Sentinel (%) 1 b Template Version # 1 c Security Version # 1 d Postal Code 11 e Class 2 f Restrictions 10 g Endorsements 4 h Sex 1 i Height 3 j Weight 3 k Hair Color 3 l Eye Color 3 m ID # 10 n Reserved Space 16 o Error Correction 6 p Security 5 q End Sentinel (?) 1 r Linear Redundancy Check (LRC) Character 1

user00265 commented 7 years ago

My card reader doesn't have a track 3 reader on the head, so the code from @winfinit was adapted to use tracks 1 and 2. I had success with a MN and WI driver's licenses as tests. It obviously returns more data than a credit card. There original library outputs Last/First/Middle as an object, this was changed to output entire field as a string in common US format of First/Middle/Last, but can easily be changed. Please note that to get ALL data, you cannot have firstLineOnly set to true -- it will break the parser.

I don't claim any ownership as this code is 99% from @winfinit and his aamvajs project. Hat off to him for this code!

@CarlRaymond, what do you think?

      // AAMVA parser. US Driver's License
      aamva: function(rawData) {
          data = rawData.replace(/\n/, "");

          // replace spaces with regular space
          data = data.replace(/\s/g, " ");

          var track = data.match(/(.*?\?)(.*?\?)/);
          var res1 = track[1].match(/(\!)([A-Z]{2})([^\^]{0,13})\^?([^\^]{0,35})\^?([^\^]{0,29})\^?\s*?\?/);
          var res2 = track[2].match(/(;)(\d{6})(\d{0,13})(\=)(\d{4})(\d{8})(\d{0,5})\=?\?/);
          var state = res1[2];

          // Return AAMVA data
          return {
              "state": res1[2],
              "city": res1[3],
              "name": function() {
                  var name = '';
                  var res = res1[4].match(/([^\$]{0,35})\$?([^\$]{0,35})?\$?([^\$]{0,35})?/);
                  if (!res) return;
                  if (res[2]) {
                      name = name + res[2];
                  }
                  if (res[3]) {
                      name = name + res[3];
                  }
                  if (res[1]) {
                      name = name + res[1];
                  }
                  return name;
              }(),
              "address": res1[5],
              "iso_iin": res2[2],
              "dl": res2[3],
              "expiration_date": function() {
                  date = res2[5];
                  if (!date) return;
                  var start = parseInt(date[0] + date[1]);
                  if (start < 13) {
                      return date[4] + date[5] + date[6] + date[7] + date[0] + date[1] + date[2] + date[3];
                  }
                  return date;
              }(),
              "birthday": function() {
                  var dob = res2[6].match(/(\d{4})(\d{2})(\d{2})/);
                  if (!dob) return;

                  if (dob[2] === '99') {
                      /* FL decided to reverse 2012 aamva spec, 99 means here
                          that dob month === to expiration month, it should be
                          opposite
                          */
                      var exp_dt = res2[5].match(/(\d{2})(\d{2})/);
                      dob[2] = exp_dt[2];
                  }
                  //dob[2]--; what was this for?
                  return dob[1] + dob[2] + dob[3];
              }(),
              "dl_overflow": res2[7],
              "id": function() {
                  var id;
                  switch (state) {
                      case "FL":
                          var res = res2[3].match(/(\d{2})(.*)/);
                          if (!res) return;
                          id = (String.fromCharCode(Number(res[1]) + 64) + res[2] + res2[7]);
                          break;
                      default:
                          id = res2[3];
                          break;
                  }
                  return id;
              }()
          };
      },
CarlRaymond commented 7 years ago

Thanks for the contribution. However, it doesn't look like your parser will return null when the data doesn't fit the format. That's necessary so that multiple parsers can be tried in succession until one succeeds.

If you can add that check, then it's looking good.