Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions __test__/at.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,22 +179,43 @@ 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) {
expect(r.error.code).toBe("INVALID_COMPONENT");
}
});

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");
}
});
Comment on lines +182 to 217
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure the new validation logic for month-13 and day-0 VNRs works correctly and doesn't regress, we should add positive test cases for valid month-13 and day-0 VNRs alongside the negative ones.

CC on behalf of @jan-kubica

  // VNRs may legitimately encode "month > 12" when the
  // daily-serial pool for a substitute date is exhausted
  // (per sozialversicherung.at). We do not enforce
  // calendar validity on the embedded date; only the
  // checksum is gating. A month-13 value with a non-
  // matching check digit therefore fails on checksum.
  test("non-matching month-13 fails on checksum", () => {
    const r = at.vnr.validate("1237011380");
    expect(r.valid).toBe(false);
    if (!r.valid) {
      expect(r.error.code).toBe("INVALID_CHECKSUM");
    }
  });

  test("matching month-13 is valid", () => {
    const r = at.vnr.validate("1234011380");
    expect(r.valid).toBe(true);
  });

  test("non-matching day-0 fails on checksum", () => {
    const r = at.vnr.validate("1237000180");
    expect(r.valid).toBe(false);
    if (!r.valid) {
      expect(r.error.code).toBe("INVALID_CHECKSUM");
    }
  });

  test("matching day-0 is valid", () => {
    const r = at.vnr.validate("1248000180");
    expect(r.valid).toBe(true);
  });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the matching-month-13 positive test (1234011380) in f18e2eb. Did not add the matching-day-0 test — the source explicitly says day stays "01" even in substitute and overflow cases, so I restored the 1-31 day-bounds check and added a 'day-0 is invalid' negative test instead (see the parallel thread from Codex). Also added a leading-zero serial test for symmetry.

CC on behalf of @jan-kubica


test("format adds space", () => {
expect(at.vnr.format("1237010180")).toBe("1237 010180");
});
Expand Down
13 changes: 7 additions & 6 deletions __test__/us.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)", () => {
Expand Down
36 changes: 30 additions & 6 deletions src/at/vnr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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") {
Comment on lines +58 to +60
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep validating the VNR day field

By removing all calendar-date checks here, checksum-valid numbers with an impossible day now pass; for example, 1002000180 encodes day 00 and validates successfully. The documented special cases require relaxing the month, but the day still needs bounds, so this accepts malformed VNRs that were rejected before.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored in f18e2eb. You're right — the source only documents month overflow (substitute dates and 13/14/etc. for serial exhaustion), and even in those cases the day stays "01". The 1-31 day-bounds check is back, so 1002000180 is rejected on day=00 again.

CC on behalf of @jan-kubica

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Constrain generated VNR serials

With this new guard, generate() can still return invalid VNRs because it builds serial with randomDigits(3), which emits values from 000 through 099 about 10% of the time. In those cases validate(generate()) now fails with INVALID_COMPONENT, breaking the generator contract advertised as a random valid Austrian VNR.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f18e2eb — generate() now builds the serial as String(randomInt(1, 9)) + randomDigits(2), so the first digit is deterministically non-zero and the CI generator roundtrip test (which was failing intermittently) is green again.

CC on behalf of @jan-kubica

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The new validation rule restricts the first digit of the serial from being '0'. However, the generate function (line 99) still uses randomDigits(3) to generate the serial, which can produce a serial starting with '0' (e.g., '012'). This will cause generate to produce invalid VNRs that fail validation.

Please update the generate function to ensure the serial does not start with '0'. For example, by using String(randomInt(100, 999)) instead of randomDigits(3).

CC on behalf of @jan-kubica

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same fix in f18e2eb — went with String(randomInt(1, 9)) + randomDigits(2) instead of randomInt(100, 999) so the trailing two digits stay uniformly random.

CC on behalf of @jan-kubica

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",
);
}

Expand Down Expand Up @@ -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");
Expand Down
14 changes: 9 additions & 5 deletions src/us/rtn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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],
Expand Down
Loading