diff --git a/__test__/at.test.ts b/__test__/at.test.ts index b68d6ef..a9daea4 100644 --- a/__test__/at.test.ts +++ b/__test__/at.test.ts @@ -179,15 +179,28 @@ describe("at.vnr", () => { } }); - test("invalid birth date (month > 12)", () => { + // VNRs may legitimately encode "month > 12" when the + // daily-serial pool for a substitute date is exhausted + // (per sozialversicherung.at). Month is therefore not + // calendar-checked; only the checksum is gating. + test("month-13 with matching checksum is valid", () => { + const r = at.vnr.validate("1234011380"); + expect(r.valid).toBe(true); + if (r.valid) expect(r.compact).toBe("1234011380"); + }); + + test("month-13 with non-matching checksum fails on checksum", () => { const r = at.vnr.validate("1237011380"); expect(r.valid).toBe(false); if (!r.valid) { - expect(r.error.code).toBe("INVALID_COMPONENT"); + expect(r.error.code).toBe("INVALID_CHECKSUM"); } }); - test("invalid birth date (day 0)", () => { + // Day, by contrast, is always 01-31 per the source — + // even substitute and overflow VNRs use day "01" — + // so day "00" is rejected as a component error. + test("day-0 is invalid", () => { const r = at.vnr.validate("1237000180"); expect(r.valid).toBe(false); if (!r.valid) { @@ -195,6 +208,14 @@ describe("at.vnr", () => { } }); + test("leading-zero serial is invalid", () => { + const r = at.vnr.validate("0000000000"); + expect(r.valid).toBe(false); + if (!r.valid) { + expect(r.error.code).toBe("INVALID_COMPONENT"); + } + }); + test("format adds space", () => { expect(at.vnr.format("1237010180")).toBe("1237 010180"); }); diff --git a/__test__/us.test.ts b/__test__/us.test.ts index 01eda5a..de07c6d 100644 --- a/__test__/us.test.ts +++ b/__test__/us.test.ts @@ -265,12 +265,13 @@ describe("us.rtn", () => { } }); - test("invalid prefix (00)", () => { - const r = us.rtn.validate("001000021"); - expect(r.valid).toBe(false); - if (!r.valid) { - expect(r.error.code).toBe("INVALID_COMPONENT"); - } + // Prefix 00 is reserved for U.S. Government and + // federal agencies per the ABA Routing Number Policy + // (https://en.wikipedia.org/wiki/ABA_routing_transit_number). + test("valid prefix (00, U.S. Government)", () => { + const r = us.rtn.validate("000000026"); + expect(r.valid).toBe(true); + if (r.valid) expect(r.compact).toBe("000000026"); }); test("invalid prefix (13)", () => { diff --git a/src/at/vnr.ts b/src/at/vnr.ts index 0dce509..8360c66 100644 --- a/src/at/vnr.ts +++ b/src/at/vnr.ts @@ -6,8 +6,19 @@ * The check digit is computed using a weighted sum * mod 11 over the 9 non-check digits. * + * The 6-digit "birth date" field is not always a real + * calendar date. Sozialversicherung.at documents that + * persons with unknown date of birth are issued + * substitute values (01.01. or 01.07.), and that + * months 13, 14, etc. are issued when the daily serial + * pool for a given substitute date is exhausted. We + * therefore do not enforce a calendar-valid month here. + * The day field, however, is always within 01-31 — even + * in the documented overflow cases the day stays "01" + * — so we keep the day-bounds check. + * * @see https://de.wikipedia.org/wiki/Sozialversicherungsnummer#%C3%96sterreich - * @see https://www.sozialversicherung.at/ + * @see https://www.sozialversicherung.at/cdscontent/?contentid=10007.820902&viewmode=content */ import type { ValidateResult, Validator } from "../types"; @@ -44,13 +55,24 @@ const validate = (value: string): ValidateResult => { ); } - // Validate birth date (DDMMYY in positions 4-9) + // Per sozialversicherung.at, the 3-digit serial + // (positions 0-2) never starts with zero. + if (v[0] === "0") { + return err( + "INVALID_COMPONENT", + "Austrian VNR serial must not start with zero", + ); + } + + // Day (positions 4-5) is always within 01-31, even + // in the documented substitute and overflow cases + // where the month is shifted. The month is left + // unchecked per the source. const day = Number(v.slice(4, 6)); - const month = Number(v.slice(6, 8)); - if (day < 1 || day > 31 || month < 1 || month > 12) { + if (day < 1 || day > 31) { return err( "INVALID_COMPONENT", - "Austrian VNR contains an invalid birth date", + "Austrian VNR day field must be within 01-31", ); } @@ -88,7 +110,9 @@ const format = (value: string): string => { /** Generate a random valid Austrian VNR. */ const generate = (): string => { for (;;) { - const serial = randomDigits(3); + // Serial must not start with zero (see validate). + const serial = + String(randomInt(1, 9)) + randomDigits(2); const day = String(randomInt(1, 28)).padStart(2, "0"); const month = String(randomInt(1, 12)).padStart(2, "0"); const year = String(randomInt(0, 99)).padStart(2, "0"); diff --git a/src/us/rtn.ts b/src/us/rtn.ts index 8b4cda2..9232d49 100644 --- a/src/us/rtn.ts +++ b/src/us/rtn.ts @@ -7,9 +7,9 @@ * [3, 7, 1] repeated three times. * * The first two digits identify the Federal Reserve - * district (01-12) or special ranges (21-32 for - * thrifts, 61-72 for electronic, 80 for traveler's - * cheques). + * district (01-12) or special ranges (00 for U.S. + * Government, 21-32 for thrifts, 61-72 for electronic, + * 80 for traveler's cheques). * * @see https://en.wikipedia.org/wiki/ABA_routing_transit_number * @see https://www.frbservices.org/ @@ -48,7 +48,11 @@ const validate = (value: string): ValidateResult => { // Validate Federal Reserve prefix (first 2 digits) const prefix = Number(v.slice(0, 2)); const validPrefixRanges = - (prefix >= 1 && prefix <= 12) || + // 00 is reserved for U.S. Government and federal + // agencies per the ABA Routing Number Policy; + // 01-12 are the 12 Federal Reserve districts. + // @see https://en.wikipedia.org/wiki/ABA_routing_transit_number + (prefix >= 0 && prefix <= 12) || (prefix >= 21 && prefix <= 32) || (prefix >= 61 && prefix <= 72) || prefix === 80; @@ -78,7 +82,7 @@ const format = (value: string): string => compact(value); /** All valid Federal Reserve prefix ranges. */ const PREFIX_RANGES: readonly [number, number][] = [ - [1, 12], + [0, 12], [21, 32], [61, 72], [80, 80],