Skip to content

Commit 8a5475d

Browse files
committed
fix: Handle TD1 embedded document check digits in optional1
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. Notes: - No official public specification found for embedded document‑number check digits in optional1. - The anonymized test case is derived from a real example that cannot be shared. This is related to #66.
1 parent f716198 commit 8a5475d

File tree

4 files changed

+61
-6
lines changed

4 files changed

+61
-6
lines changed

src/parse/__tests__/td1.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,4 +470,39 @@ describe('parse TD1', () => {
470470
],
471471
]);
472472
});
473+
474+
it('TD1 with embedded document check digit', () => {
475+
// Check digit is embedded in optional1
476+
const MRZ = [
477+
'I<PRT123456789<AAB4<<<<<<<<<<<',
478+
'9001011M3001019PRT<<<<<<<<<<<2',
479+
'ANON<USER<<TEST<CASE<<<<<<<<<<',
480+
];
481+
482+
const result = parse(MRZ);
483+
484+
expect(result).toMatchObject({
485+
format: 'TD1',
486+
valid: true,
487+
documentNumber: result.fields.documentNumber,
488+
});
489+
490+
expect(result.fields).toStrictEqual({
491+
documentCode: 'I',
492+
issuingState: 'PRT',
493+
documentNumber: '123456789AAB',
494+
documentNumberCheckDigit: '4',
495+
birthDate: '900101',
496+
birthDateCheckDigit: '1',
497+
sex: 'male',
498+
expirationDate: '300101',
499+
expirationDateCheckDigit: '9',
500+
nationality: 'PRT',
501+
optional1: 'AAB4',
502+
optional2: '',
503+
compositeCheckDigit: '2',
504+
lastName: 'ANON USER',
505+
firstName: 'TEST CASE',
506+
});
507+
});
473508
});

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: 14 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,19 @@ 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+
// Some TD1 issuers embed the document check digit inside optional1
12+
const embeddedDigit = optional.charAt(firstFiller - 1);
13+
const embeddedValid =
14+
computeCheckDigit(`${source}${tail}`) === Number(embeddedDigit);
15+
if (embeddedValid) {
16+
return {
17+
value: embeddedDigit,
18+
start: firstFiller,
19+
end: firstFiller + 1,
20+
};
21+
}
22+
source = `${source}<${tail}`;
23+
checkDigit = embeddedDigit;
1324
check(source, checkDigit);
1425
return {
1526
value: checkDigit,

0 commit comments

Comments
 (0)