verant_id_cloud_scan 1.4.4 → 1.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Api.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { compressAndCompareImages } from "./FaceComparison.js";
2
2
  import { formatDateToISO, DATE_FIELDS } from "./DateUtils.js";
3
3
  import { FIELD_MAPPING, FIELD_DEFAULTS, getFieldValue } from "./FieldMapping.js";
4
+ import { sanitizeFieldForDmv } from "./FormatUtils.js";
4
5
 
5
6
  //prod
6
7
  const licenseServerAddress = "https://lic.verantid.com/api/v1";
@@ -12,7 +13,7 @@ const dmvServerAddress = "https://dmv.verantid.com/api/v1/dmv_check";
12
13
  // "https://dmv-check-server-staging-fcaab48bec21.herokuapp.com/api/v1/dmv_check";
13
14
 
14
15
  export const dmvCheck = async (clientId, scannerType, licenseData) => {
15
- const url = dmvServerAddress; // make sure dmvServerAddress is defined elsewhere
16
+ const url = dmvServerAddress;
16
17
 
17
18
  // Build the driver object dynamically.
18
19
  const driver = {};
@@ -36,16 +37,8 @@ export const dmvCheck = async (clientId, scannerType, licenseData) => {
36
37
  value = formatDateToISO(value);
37
38
  }
38
39
 
39
- // Convert sex_code from M/F to 1/2 for AAMVA compliance
40
- if (fieldName === 'sex_code' && value) {
41
- const sexStr = String(value).trim().toUpperCase();
42
- if (sexStr === 'M' || sexStr === 'MALE') {
43
- value = '1';
44
- } else if (sexStr === 'F' || sexStr === 'FEMALE') {
45
- value = '2';
46
- }
47
- // If already 1 or 2, keep as is
48
- }
40
+ // Sanitize field value for DMV/AAMVA submission
41
+ value = sanitizeFieldForDmv(fieldName, value);
49
42
 
50
43
  driver[fieldName] = value;
51
44
  });
package/CloudScan.js CHANGED
@@ -210,20 +210,33 @@ function getField(keyword, foundBarcodeString, beginNewline = true) {
210
210
  keywordOffset = keyword.length + 1; // +1 for \n
211
211
 
212
212
  if (foundIndex === -1) {
213
- // Try 2: After DL subfile marker
214
- foundIndex = foundBarcodeString.indexOf("DL" + keyword);
215
- if (foundIndex !== -1) {
213
+ // Try 2: After DL subfile marker (common for first field on line)
214
+ const dlIndex = foundBarcodeString.indexOf("DL" + keyword);
215
+ if (dlIndex !== -1) {
216
+ // Make sure it's actually after a DL marker, not in the middle of data
217
+ // Check if "DL" appears right before our keyword
218
+ foundIndex = dlIndex;
216
219
  keywordOffset = keyword.length + 2; // +2 for "DL"
217
220
  }
218
221
  }
219
222
 
220
223
  if (foundIndex === -1) {
221
224
  // Try 3: After ZO subfile marker
222
- foundIndex = foundBarcodeString.indexOf("ZO" + keyword);
223
- if (foundIndex !== -1) {
225
+ const zoIndex = foundBarcodeString.indexOf("ZO" + keyword);
226
+ if (zoIndex !== -1) {
227
+ foundIndex = zoIndex;
224
228
  keywordOffset = keyword.length + 2; // +2 for "ZO"
225
229
  }
226
230
  }
231
+
232
+ if (foundIndex === -1) {
233
+ // Try 4: After ZW subfile marker (Washington enhanced licenses)
234
+ const zwIndex = foundBarcodeString.indexOf("ZW" + keyword);
235
+ if (zwIndex !== -1) {
236
+ foundIndex = zwIndex;
237
+ keywordOffset = keyword.length + 2; // +2 for "ZW"
238
+ }
239
+ }
227
240
  } else {
228
241
  // When beginNewline=false (used for DAQ workaround)
229
242
  foundIndex = foundBarcodeString.indexOf(keyword);
@@ -232,21 +245,32 @@ function getField(keyword, foundBarcodeString, beginNewline = true) {
232
245
 
233
246
  if (foundIndex === -1) return false;
234
247
 
235
- // Find the next delimiter - could be \n (newline) or \r (carriage return)
236
- var nextNewline = foundBarcodeString.indexOf("\n", foundIndex + 1);
237
- var nextCarriageReturn = foundBarcodeString.indexOf("\r", foundIndex + 1);
238
-
239
- // Use whichever delimiter comes first (or only one if the other is -1)
240
- var m;
241
- if (nextNewline === -1) {
242
- m = nextCarriageReturn;
243
- } else if (nextCarriageReturn === -1) {
244
- m = nextNewline;
248
+ // Find the end of this field's value
249
+ // Strategy: Look for the next field code (3 uppercase letters starting with D or Z)
250
+ // Field codes can appear after: newline, carriage return, or space (for jurisdiction-specific Z* fields only)
251
+
252
+ const startPos = foundIndex + keywordOffset;
253
+ const remainingString = foundBarcodeString.substring(startPos);
254
+
255
+ // Look for the next field code pattern
256
+ // Standard field codes: DA*, DB*, DC*, DD*, DE*, DF*, DG*, DH*, DI*, DJ*, DK*, DL*, DM*, DN*, DO*, DP*
257
+ // Jurisdiction-specific: ZA*, ZB*, ZC*, ..., ZW*, ZX*, ZY*, ZZ*
258
+ // Pattern matches:
259
+ // - \nDAA, \nZNB (newline + field code starting with D or Z)
260
+ // - \rDAA (carriage return + field code)
261
+ // - ZWZ (space + field code starting with Z - for jurisdiction codes that may appear mid-line)
262
+ const nextFieldPattern = /[\n\r]([D][A-Z]{2}|[Z][A-Z]{2})|\s([Z][A-Z]{2})/;
263
+ const match = remainingString.match(nextFieldPattern);
264
+
265
+ let endPos;
266
+ if (match) {
267
+ endPos = startPos + match.index;
245
268
  } else {
246
- m = Math.min(nextNewline, nextCarriageReturn);
269
+ // No next field found, use end of string
270
+ endPos = foundBarcodeString.length;
247
271
  }
248
272
 
249
- var subtext = foundBarcodeString.substring(foundIndex + keywordOffset, m);
273
+ var subtext = foundBarcodeString.substring(startPos, endPos).trim();
250
274
  return subtext;
251
275
  }
252
276
 
@@ -269,13 +293,25 @@ export function extractInformation(foundBarcodeString) {
269
293
  resultsArray[item.description] = formatHeightForDisplay(fieldValue);
270
294
  }
271
295
  // Parse FullName (DAA) into separate FirstName/LastName/MiddleName fields
272
- // AAMVA standard format: LastName,FirstName,MiddleName
273
- else if (item.description === 'FullName' && fieldValue.includes(',')) {
274
- const parts = fieldValue.split(',').map(p => p.trim());
275
- // Only set these if they don't already exist (some states use separate fields)
276
- if (!resultsArray['LastName'] && parts[0]) resultsArray['LastName'] = parts[0];
277
- if (!resultsArray['FirstName'] && parts[1]) resultsArray['FirstName'] = parts[1];
278
- if (!resultsArray['MiddleName'] && parts[2]) resultsArray['MiddleName'] = parts[2];
296
+ else if (item.description === 'FullName') {
297
+ // AAMVA standard format: LastName,FirstName,MiddleName
298
+ if (fieldValue.includes(',')) {
299
+ const parts = fieldValue.split(',').map(p => p.trim());
300
+ // Only set these if they don't already exist (some states use separate fields)
301
+ if (!resultsArray['LastName'] && parts[0]) resultsArray['LastName'] = parts[0];
302
+ if (!resultsArray['FirstName'] && parts[1]) resultsArray['FirstName'] = parts[1];
303
+ if (!resultsArray['MiddleName'] && parts[2]) resultsArray['MiddleName'] = parts[2];
304
+ } else {
305
+ // Some states use space-separated format: "FIRST MIDDLE LAST"
306
+ const parts = fieldValue.split(/\s+/);
307
+ if (parts.length >= 2) {
308
+ if (!resultsArray['FirstName']) resultsArray['FirstName'] = parts[0];
309
+ if (!resultsArray['LastName']) resultsArray['LastName'] = parts[parts.length - 1];
310
+ if (parts.length >= 3 && !resultsArray['MiddleName']) {
311
+ resultsArray['MiddleName'] = parts.slice(1, -1).join(' ');
312
+ }
313
+ }
314
+ }
279
315
  // Also store the full name
280
316
  resultsArray[item.description] = fieldValue;
281
317
  }
@@ -291,6 +327,27 @@ export function extractInformation(foundBarcodeString) {
291
327
  }
292
328
  }
293
329
 
330
+ // Extract jurisdiction code from ANSI header if not found in fields
331
+ if (!resultsArray['MailingJurisdictionCode'] && !resultsArray['JurisdictionCode']) {
332
+ const ansiMatch = foundBarcodeString.match(/^ANSI\s+(\d{6})/);
333
+ if (ansiMatch) {
334
+ const issuerCode = ansiMatch[1];
335
+ // Map AAMVA IIN (Issuer Identification Number) to state codes
336
+ const iinToState = {
337
+ '636038': 'OH', // Ohio
338
+ '636026': 'CT', // Connecticut
339
+ '636045': 'WA', // Washington
340
+ '636036': 'NJ', // New Jersey
341
+ '636005': 'SC', // South Carolina
342
+ // Add more as needed
343
+ };
344
+ const state = iinToState[issuerCode];
345
+ if (state) {
346
+ resultsArray['MailingJurisdictionCode'] = state;
347
+ }
348
+ }
349
+ }
350
+
294
351
  return resultsArray;
295
352
  }
296
353
 
package/FormatUtils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * FormatUtils.js
3
- * Utility functions for formatting barcode field data for display
3
+ * Utility functions for formatting barcode field data for display and AAMVA submission
4
4
  */
5
5
 
6
6
  /**
@@ -52,3 +52,147 @@ export function formatHeightForDisplay(height) {
52
52
  // Return original value for any other format
53
53
  return height;
54
54
  }
55
+
56
+ /**
57
+ * Sanitizes field values for DMV/AAMVA submission
58
+ * Cleans up common issues with barcode data to ensure AAMVA acceptance
59
+ * @param {string} fieldName - The standardized field name (e.g., 'height', 'sex_code')
60
+ * @param {string} value - The field value to sanitize
61
+ * @returns {string} Sanitized value ready for AAMVA submission
62
+ */
63
+ export function sanitizeFieldForDmv(fieldName, value) {
64
+ if (!value) return value;
65
+
66
+ switch (fieldName) {
67
+ case 'height':
68
+ return sanitizeHeight(value);
69
+
70
+ case 'sex_code':
71
+ return sanitizeSexCode(value);
72
+
73
+ case 'eye_color':
74
+ return sanitizeEyeColor(value);
75
+
76
+ case 'license_number':
77
+ return sanitizeLicenseNumber(value);
78
+
79
+ case 'postal_code':
80
+ return sanitizePostalCode(value);
81
+
82
+ case 'last_name':
83
+ return sanitizeLastName(value);
84
+
85
+ default:
86
+ return value;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Converts height to total inches for AAMVA
92
+ * Handles formats: "509" (5'9"), "066 in" (66 inches), "067" (67 inches)
93
+ * @param {string} height - Height value from barcode
94
+ * @returns {string} Total inches as numeric string
95
+ */
96
+ function sanitizeHeight(height) {
97
+ let heightStr = String(height).trim();
98
+
99
+ // Remove " in", " cm", or any other suffix
100
+ heightStr = heightStr.replace(/\s*(in|cm|IN|CM)\s*$/i, '').trim();
101
+
102
+ // Check if it's in AAMVA 3-digit feet/inches format (e.g., "509" = 5'09")
103
+ if (/^\d{3}$/.test(heightStr)) {
104
+ const feet = parseInt(heightStr.charAt(0), 10);
105
+ const inches = parseInt(heightStr.substring(1), 10);
106
+ return String(feet * 12 + inches); // Convert to total inches
107
+ }
108
+
109
+ // If it's already a numeric value (with possible leading zeros), parse it
110
+ if (/^\d+$/.test(heightStr)) {
111
+ return String(parseInt(heightStr, 10)); // Remove leading zeros
112
+ }
113
+
114
+ // Otherwise keep as-is
115
+ return height;
116
+ }
117
+
118
+ /**
119
+ * Converts sex code from M/F to 1/2 for AAMVA
120
+ * @param {string} sexCode - Sex code from barcode
121
+ * @returns {string} AAMVA numeric code (1=Male, 2=Female)
122
+ */
123
+ function sanitizeSexCode(sexCode) {
124
+ const sexStr = String(sexCode).trim().toUpperCase();
125
+
126
+ if (sexStr === 'M' || sexStr === 'MALE') {
127
+ return '1';
128
+ } else if (sexStr === 'F' || sexStr === 'FEMALE') {
129
+ return '2';
130
+ }
131
+
132
+ // If already 1 or 2, keep as is
133
+ return sexCode;
134
+ }
135
+
136
+ /**
137
+ * Validates eye color against AAMVA standard codes and logs an error if invalid
138
+ * Only transforms BRN → BRO (known alternative spelling for Brown)
139
+ * Example: "BRN" → "BRO"
140
+ * @param {string} eyeColor - Eye color from barcode
141
+ * @returns {string} Eye color value (unchanged unless BRN)
142
+ */
143
+ function sanitizeEyeColor(eyeColor) {
144
+ // AAMVA-accepted eye color codes (official list)
145
+ const VALID_EYE_COLORS = ['BLK', 'BLU', 'BRO', 'DIC', 'GRY', 'GRN', 'HAZ', 'MAR', 'PNK', 'UNK'];
146
+
147
+ let value = String(eyeColor).trim();
148
+
149
+ // Map BRN → BRO (known alternative spelling for Brown)
150
+ if (value === 'BRN') {
151
+ value = 'BRO';
152
+ }
153
+
154
+ // Validate against AAMVA standard list and log error if invalid
155
+ if (!VALID_EYE_COLORS.includes(value)) {
156
+ console.error(
157
+ `[AAMVA Validation Error] Eye color "${value}" is not a valid AAMVA code. ` +
158
+ `Accepted values: BLK (Black), BLU (Blue), BRO (Brown), DIC (Dichromatic), ` +
159
+ `GRY (Gray), GRN (Green), HAZ (Hazel), MAR (Maroon), PNK (Pink), UNK (Unknown). ` +
160
+ `DMV verification will likely fail.`
161
+ );
162
+ }
163
+
164
+ return value;
165
+ }
166
+
167
+ /**
168
+ * Trims leading and trailing spaces from license number
169
+ * Preserves internal spaces as some jurisdictions may require them
170
+ * Example: " QWE 234 KHL " → "QWE 234 KHL"
171
+ * @param {string} licenseNumber - License number from barcode
172
+ * @returns {string} License number with leading/trailing spaces removed
173
+ */
174
+ function sanitizeLicenseNumber(licenseNumber) {
175
+ return String(licenseNumber).trim();
176
+ }
177
+
178
+ /**
179
+ * Sanitizes postal code by removing dashes and trimming spaces
180
+ * AAMVA may not accept dashes in zip codes
181
+ * Example: "08223-1518" → "082231518", " 12345 " → "12345"
182
+ * @param {string} postalCode - Postal code from barcode
183
+ * @returns {string} Sanitized postal code without dashes or extra spaces
184
+ */
185
+ function sanitizePostalCode(postalCode) {
186
+ return String(postalCode).replace(/-/g, '').trim();
187
+ }
188
+
189
+ /**
190
+ * Sanitizes last name by removing spaces (combines suffix with last name)
191
+ * When suffix is included in last name field, remove spaces for AAMVA
192
+ * Example: "MYLAST JR" → "MYLASTJR", "O'BRIEN III" → "O'BRIENIII"
193
+ * @param {string} lastName - Last name from barcode
194
+ * @returns {string} Last name with spaces removed
195
+ */
196
+ function sanitizeLastName(lastName) {
197
+ return String(lastName).replace(/\s+/g, '');
198
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verant_id_cloud_scan",
3
- "version": "1.4.4",
3
+ "version": "1.4.5",
4
4
  "description": "Verant ID Cloud Scan NPM Library",
5
5
  "main": "CloudScan.js",
6
6
  "types": "index.d.ts",