diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..3c5a4f7 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + testURL: 'http://localhost', +}; diff --git a/package.json b/package.json index 7a8582c..e33163a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "mrz", - "version": "3.1.1", + "name": "@lukesolo/mrz", + "version": "3.1.3", "description": "Parse MRZ (Machine Readable Zone) from identity documents", "main": "./src/index.js", "files": [ diff --git a/src/formats.js b/src/formats.js index 60405c4..dc1b6f2 100644 --- a/src/formats.js +++ b/src/formats.js @@ -5,7 +5,8 @@ const formats = { TD2: 'TD2', TD3: 'TD3', SWISS_DRIVING_LICENSE: 'SWISS_DRIVING_LICENSE', - FRENCH_NATIONAL_ID: 'FRENCH_NATIONAL_ID' + FRENCH_NATIONAL_ID: 'FRENCH_NATIONAL_ID', + FRENCH_DRIVING_LICENSE: 'FRENCH_DRIVING_LICENSE' }; Object.freeze(formats); diff --git a/src/parse/__tests__/frenchDrivingLicense.js b/src/parse/__tests__/frenchDrivingLicense.js new file mode 100644 index 0000000..89ffce6 --- /dev/null +++ b/src/parse/__tests__/frenchDrivingLicense.js @@ -0,0 +1,22 @@ +'use strict'; + +const parse = require('../parse'); + +describe('parse French Driving License', () => { + it('valid MRZ', function () { + const MRZ = [ 'D1FRA13BB148959280920BETTOLO<7' ]; + var result = parse(MRZ); + expect(result.format).toBe('FRENCH_DRIVING_LICENSE'); + expect(result.details.filter((a) => !a.valid)).toHaveLength(0); + expect(result.fields).toEqual({ + documentCode: 'D', + bapConfiguration: '1', + issuingState: 'FRA', + documentNumber: '13BB14895', + documentNumberCheckDigit: '9', + expirationDate: '280920', + lastName: 'BETTOLO', + compositeCheckDigit: '7' + }); + }); +}); diff --git a/src/parse/__tests__/frenchNationalId.js b/src/parse/__tests__/frenchNationalId.js index a017f02..207e844 100644 --- a/src/parse/__tests__/frenchNationalId.js +++ b/src/parse/__tests__/frenchNationalId.js @@ -19,7 +19,7 @@ describe('parse French National Id', () => { administrativeCode: '0CHE02', issueDate: '1710', administrativeCode2: 'GVA', - documentNumber: '12345', + documentNumber: '1710GVA12345', documentNumberCheckDigit: '1', firstName: 'ROBERTA', birthDate: '911231', @@ -28,4 +28,30 @@ describe('parse French National Id', () => { compositeCheckDigit: '2' }); }); + + it('valid MRZ of version without administrativeCode', function () { + const MRZ = [ + 'IDFRABERTHIER<<<<<<<<<<<<<<<<<<<<<<<', + '9409923102854CORINNE<<<<<<<6512068F4' + ]; + var result = parse(MRZ); + expect(result.format).toBe('FRENCH_NATIONAL_ID'); + // expect(result.valid).toEqual(true); + expect(result.details.filter((a) => !a.valid)).toHaveLength(0); + expect(result.fields).toEqual({ + documentCode: 'ID', + issuingState: 'FRA', + lastName: 'BERTHIER', + administrativeCode: '', + issueDate: '9409', + administrativeCode2: '923', + documentNumber: '940992310285', + documentNumberCheckDigit: '4', + firstName: 'CORINNE', + birthDate: '651206', + birthDateCheckDigit: '8', + sex: 'female', + compositeCheckDigit: '4' + }); + }); }); diff --git a/src/parse/fieldTemplates.js b/src/parse/fieldTemplates.js index 6d0605e..0189acb 100644 --- a/src/parse/fieldTemplates.js +++ b/src/parse/fieldTemplates.js @@ -83,6 +83,11 @@ const issuingStateTemplate = { parser: require('../parsers/parseState') }; +const bapConfigurationTemplate = { + label: 'BAP configuration', + field: 'bapConfiguration' +}; + module.exports = { documentNumberTemplate, documentNumberCheckDigitTemplate, @@ -97,5 +102,6 @@ module.exports = { compositeCheckDigitTemplate, firstNameTemplate, lastNameTemplate, - issuingStateTemplate + issuingStateTemplate, + bapConfigurationTemplate }; diff --git a/src/parse/frenchDrivingLicense.js b/src/parse/frenchDrivingLicense.js new file mode 100644 index 0000000..7890745 --- /dev/null +++ b/src/parse/frenchDrivingLicense.js @@ -0,0 +1,25 @@ +'use strict'; + +const checkLines = require('./checkLines'); +const getResult = require('./getResult'); +const { FRENCH_DRIVING_LICENSE } = require('../formats'); +const frenchDrivingLicenseFields = require('./frenchDrivingLicenseFields'); + +module.exports = function parseFrenchDrivingLicense(lines) { + lines = checkLines(lines); + if (lines.length !== 1) { + throw new Error( + `invalid number of lines: ${ + lines.length + }: Must be 1 for ${FRENCH_DRIVING_LICENSE}` + ); + } + if (lines[0].length !== 30) { + throw new Error( + `invalid number of characters for line: ${ + lines[0].length + }. Must be 30 for ${FRENCH_DRIVING_LICENSE}` + ); + } + return getResult(FRENCH_DRIVING_LICENSE, lines, frenchDrivingLicenseFields); +}; diff --git a/src/parse/frenchDrivingLicenseFields.js b/src/parse/frenchDrivingLicenseFields.js new file mode 100644 index 0000000..03f7cb8 --- /dev/null +++ b/src/parse/frenchDrivingLicenseFields.js @@ -0,0 +1,81 @@ +'use strict'; + +const parseDocumentCode = require('../parsers/euDrivingLicense/parseDocumentCode'); +const parseBapConfiguration = require('../parsers/euDrivingLicense/parseBapConfiguration'); +const parseState = require('../parsers/parseState'); +const parseDocumentNumber = require('../parsers/euDrivingLicense/france/parseDocumentNumber'); +const parseLastName = require('../parsers/euDrivingLicense/france/parseLastName'); + +const { + documentCodeTemplate, + bapConfigurationTemplate, + issuingStateTemplate, + documentNumberTemplate, + documentNumberCheckDigitTemplate, + expirationDateTemplate, + lastNameTemplate, + compositeCheckDigitTemplate +} = require('./fieldTemplates'); +const createFieldParser = require('./createFieldParser'); + +module.exports = [ + Object.assign({}, documentCodeTemplate, { + line: 0, + start: 0, + end: 1, + parser: parseDocumentCode + }), + Object.assign({}, bapConfigurationTemplate, { + line: 0, + start: 1, + end: 2, + parser: parseBapConfiguration + }), + Object.assign({}, issuingStateTemplate, { + line: 0, + start: 2, + end: 5, + parser: parseState + }), + Object.assign({}, documentNumberTemplate, { + line: 0, + start: 5, + end: 14, + parser: parseDocumentNumber + }), + Object.assign({}, documentNumberCheckDigitTemplate, { + line: 0, + start: 14, + end: 15, + related: [ + { + line: 0, + start: 5, + end: 14 + } + ] + }), + Object.assign({}, expirationDateTemplate, { + line: 0, + start: 15, + end: 21 + }), + Object.assign({}, lastNameTemplate, { + line: 0, + start: 21, + end: 29, + parser: parseLastName + }), + Object.assign({}, compositeCheckDigitTemplate, { + line: 0, + start: 29, + end: 30, + related: [ + { + line: 0, + start: 0, + end: 29 + } + ] + }) +].map(createFieldParser); diff --git a/src/parse/frenchNationalIdFields.js b/src/parse/frenchNationalIdFields.js index 8916e65..82f2ea3 100644 --- a/src/parse/frenchNationalIdFields.js +++ b/src/parse/frenchNationalIdFields.js @@ -59,7 +59,7 @@ module.exports = [ }, Object.assign({}, documentNumberTemplate, { line: 1, - start: 7, + start: 0, end: 12 }), Object.assign({}, documentNumberCheckDigitTemplate, { diff --git a/src/parse/parse.js b/src/parse/parse.js index 14fb022..60e842b 100644 --- a/src/parse/parse.js +++ b/src/parse/parse.js @@ -7,14 +7,21 @@ const parsers = require('./parsers'); function parseMRZ(lines) { lines = checkLines(lines); switch (lines.length) { + case 1:{ + if (lines[0].match(/^D[1PN<]FRA/)) { + return parsers.FRENCH_DRIVING_LICENSE(lines); + } + throw new Error( + 'unrecognized document format. Input must match pattern /^D[1PN<]FRA/ (French Driving License)' + ); + } case 2: case 3: { switch (lines[0].length) { case 30: return parsers.TD1(lines); case 36: { - const endLine1 = lines[0].substr(30, 36); - if (endLine1.match(/[0-9]/)) { + if (lines[0].match(/^I.FRA/)) { return parsers.FRENCH_NATIONAL_ID(lines); } else { return parsers.TD2(lines); diff --git a/src/parse/parsers.js b/src/parse/parsers.js index 510e159..d54c155 100644 --- a/src/parse/parsers.js +++ b/src/parse/parsers.js @@ -5,11 +5,13 @@ const parseTD2 = require('./td2'); const parseTD3 = require('./td3'); const parseSwissDrivingLicense = require('./swissDrivingLicense'); const parseFrenchNationalId = require('./frenchNationalId'); +const parseFrenchDrivingLicense = require('./frenchDrivingLicense'); module.exports = { TD1: parseTD1, TD2: parseTD2, TD3: parseTD3, SWISS_DRIVING_LICENSE: parseSwissDrivingLicense, - FRENCH_NATIONAL_ID: parseFrenchNationalId + FRENCH_NATIONAL_ID: parseFrenchNationalId, + FRENCH_DRIVING_LICENSE: parseFrenchDrivingLicense }; diff --git a/src/parsers/euDrivingLicense/france/parseDocumentNumber.js b/src/parsers/euDrivingLicense/france/parseDocumentNumber.js new file mode 100644 index 0000000..4a9d7ce --- /dev/null +++ b/src/parsers/euDrivingLicense/france/parseDocumentNumber.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = function parseDocumentCode(source) { + // french driving license number + if (!source.match(/^[0-9]{2}[A-Z]{2}[0-9]{5}$/)) { + throw new Error( + `invalid document number: ${source}.` + ); + } + return source; +}; diff --git a/src/parsers/euDrivingLicense/france/parseLastName.js b/src/parsers/euDrivingLicense/france/parseLastName.js new file mode 100644 index 0000000..e3fc357 --- /dev/null +++ b/src/parsers/euDrivingLicense/france/parseLastName.js @@ -0,0 +1,12 @@ +'use strict'; + +var parseText = require('../../parseText'); + +module.exports = function parseLastName(source) { + const parsed = parseText(source, /^[A-Z<]+$/); + return { + value: parsed, + start: 0, + end: parsed.length + }; +}; diff --git a/src/parsers/euDrivingLicense/parseBapConfiguration.js b/src/parsers/euDrivingLicense/parseBapConfiguration.js new file mode 100644 index 0000000..5982e68 --- /dev/null +++ b/src/parsers/euDrivingLicense/parseBapConfiguration.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = function parseBapConfiguration(bapConfig) { + switch (bapConfig) { + case '1': + case 'P': + case 'N': + case '<': + return bapConfig; + default: + throw new Error( + `invalid BAP configuration code: ${bapConfig}. Must be 1, P, N or <` + ); + } +}; diff --git a/src/parsers/euDrivingLicense/parseDocumentCode.js b/src/parsers/euDrivingLicense/parseDocumentCode.js new file mode 100644 index 0000000..35f1b55 --- /dev/null +++ b/src/parsers/euDrivingLicense/parseDocumentCode.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = function parseDocumentCode(source) { + if (source !== 'D') { + throw new Error(`invalid document code: ${source}. Must be D`); + } + return source; +}; diff --git a/src/parsers/parseDocumentCodeId.js b/src/parsers/parseDocumentCodeId.js index 41d6202..c02897f 100644 --- a/src/parsers/parseDocumentCodeId.js +++ b/src/parsers/parseDocumentCodeId.js @@ -2,9 +2,9 @@ module.exports = function parseDocumentCodeId(source) { const first = source.charAt(0); - if (first !== 'A' && first !== 'C' && first !== 'I') { + if (first !== 'A' && first !== 'C' && first !== 'I' && first !== 'R') { throw new Error( - `invalid document code: ${source}. First character must be A, C or I` + `invalid document code: ${source}. First character must be A, C, R or I` ); }