Skip to content

Commit f89e4b7

Browse files
committed
fix: Handle TD1 embedded document check digits in optional1 for non-compliant documents like PRT and BEL IDs
Some TD1 issuers embed the document number check digit inside optional1 when the check-digit slot is `<`. This update validates the embedded digit against the concatenated document number + optional prefix before falling back to the ICAO-style separator variant. This is related to #66.
1 parent f716198 commit f89e4b7

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
@@ -470,4 +470,40 @@ describe('parse TD1', () => {
470470
],
471471
]);
472472
});
473+
474+
it('Belgium ID BEL-BO-03003 with embedded document check digit', () => {
475+
// source: https://www.consilium.europa.eu/prado/en/BEL-BO-03003/index.html
476+
// This Belgian ID has the document number check digit embedded in optional1
477+
const MRZ = [
478+
'IDBEL000590240<6013<<<<<<<<<<<',
479+
'8512017F1311048BEL851201002007',
480+
'REINARTZ<<ULRIKE<KATIA<E<<<<<<',
481+
];
482+
483+
const result = parse(MRZ);
484+
485+
expect(result).toMatchObject({
486+
format: 'TD1',
487+
valid: true,
488+
documentNumber: result.fields.documentNumber,
489+
});
490+
491+
expect(result.fields).toStrictEqual({
492+
documentCode: 'ID',
493+
issuingState: 'BEL',
494+
documentNumber: '000590240601',
495+
documentNumberCheckDigit: '3',
496+
birthDate: '851201',
497+
birthDateCheckDigit: '7',
498+
sex: 'female',
499+
expirationDate: '131104',
500+
expirationDateCheckDigit: '8',
501+
nationality: 'BEL',
502+
optional1: '6013',
503+
optional2: '85120100200',
504+
compositeCheckDigit: '7',
505+
lastName: 'REINARTZ',
506+
firstName: 'ULRIKE KATIA E',
507+
});
508+
});
473509
});

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)