Skip to content

Commit 8d0aa52

Browse files
authored
fix: accept TD1 alternative check digit computation variant when embedded in optional1 (#70)
Some TD1 issuers embed the document number check digit inside optional1 when the check-digit slot is `<`. In some older authentic documents (like PRT and BEL ID cards), the `<` character is included to compute the check digit, which is not complying with ICAO specs. We now check both variants and consider the check digit valid if either matches the check digit number. This is related to #66.
1 parent 151c505 commit 8d0aa52

File tree

4 files changed

+66
-6
lines changed

4 files changed

+66
-6
lines changed

src/parse/__tests__/td1.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,4 +515,40 @@ describe('parse TD1', () => {
515515
],
516516
]);
517517
});
518+
519+
it('Belgium ID BEL-BO-03003 with embedded document check digit', () => {
520+
// source: https://www.consilium.europa.eu/prado/en/BEL-BO-03003/index.html
521+
// This Belgian ID has the document number check digit embedded in optional1
522+
const MRZ = [
523+
'IDBEL000590240<6013<<<<<<<<<<<',
524+
'8512017F1311048BEL851201002007',
525+
'REINARTZ<<ULRIKE<KATIA<E<<<<<<',
526+
];
527+
528+
const result = parse(MRZ);
529+
530+
expect(result).toMatchObject({
531+
format: 'TD1',
532+
valid: true,
533+
documentNumber: result.fields.documentNumber,
534+
});
535+
536+
expect(result.fields).toStrictEqual({
537+
documentCode: 'ID',
538+
issuingState: 'BEL',
539+
documentNumber: '000590240601',
540+
documentNumberCheckDigit: '3',
541+
birthDate: '851201',
542+
birthDateCheckDigit: '7',
543+
sex: 'female',
544+
expirationDate: '131104',
545+
expirationDateCheckDigit: '8',
546+
nationality: 'BEL',
547+
optional1: '6013',
548+
optional2: '85120100200',
549+
compositeCheckDigit: '7',
550+
lastName: 'REINARTZ',
551+
firstName: 'ULRIKE KATIA E',
552+
});
553+
});
518554
});

src/parsers/__tests__/check.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect, test } from 'vitest';
22

3-
import { check } from '../check.ts';
3+
import { check, computeCheckDigit } from '../check.ts';
44

55
test('check digits', () => {
66
expect(() => check('592166117<231', 8)).not.toThrow();
@@ -11,3 +11,8 @@ test('check digits', () => {
1111
expect(() => check('600001795015', 2)).not.toThrow();
1212
expect(() => check('592166111<773', 4)).toThrow(/invalid check digit/);
1313
});
14+
15+
test('compute embedded TD1 check digit', () => {
16+
expect(computeCheckDigit('123456789AAB')).toBe(8);
17+
expect(computeCheckDigit('123456789<AAB')).toBe(4);
18+
});

src/parsers/check.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export function check(string: string, value: string | number) {
1+
export function computeCheckDigit(string: string) {
22
let code = 0;
33
const factors = [7, 3, 1];
44
for (let i = 0; i < string.length; i++) {
@@ -10,7 +10,11 @@ export function check(string: string, value: string | number) {
1010
charCode *= factors[i % 3];
1111
code += charCode;
1212
}
13-
code %= 10;
13+
return code % 10;
14+
}
15+
16+
export function check(string: string, value: string | number) {
17+
const code = computeCheckDigit(string);
1418
if (code !== Number(value)) {
1519
throw new Error(`invalid check digit: ${value}. Must be ${code}`);
1620
}

src/parsers/parseDocumentNumberCheckDigit.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { check } from './check.ts';
1+
import { check, computeCheckDigit } from './check.ts';
22

33
export default function parseDocumentNumberCheckDigit(
44
checkDigit: string,
@@ -8,8 +8,23 @@ export default function parseDocumentNumberCheckDigit(
88
if (checkDigit === '<' && optional) {
99
const firstFiller = optional.indexOf('<');
1010
const tail = optional.slice(0, firstFiller - 1);
11-
source = `${source}${tail}`;
12-
checkDigit = optional.charAt(firstFiller - 1);
11+
// Handle older non-compliant documents (e.g., PRT and BEL IDs) where the check digit
12+
// is embedded in optional1 instead of following the document number directly.
13+
// According to ICAO Doc 9303 Part 11 (https://www.icao.int/sites/default/files/publications/DocSeries/9303_p11_cons_en.pdf)
14+
// page 88, the check digit should be calculated on the document number including
15+
// any additional characters from optional1 up to (but not including) the embedded check digit.
16+
const embeddedDigit = optional.charAt(firstFiller - 1);
17+
const embeddedValid =
18+
computeCheckDigit(`${source}${tail}`) === Number(embeddedDigit);
19+
if (embeddedValid) {
20+
return {
21+
value: embeddedDigit,
22+
start: firstFiller,
23+
end: firstFiller + 1,
24+
};
25+
}
26+
source = `${source}<${tail}`;
27+
checkDigit = embeddedDigit;
1328
check(source, checkDigit);
1429
return {
1530
value: checkDigit,

0 commit comments

Comments
 (0)