Skip to content

Commit 474496d

Browse files
committed
fix: correct broken ISIN checksum (validator accepted any check digit)
_isin_checksum computed a per-character val in the loop but never added it into check, which stayed 0. So (check % 10) == 0 was always True and isin() accepted any 12-character string with valid characters regardless of its check digit. The existing invalid-ISIN tests all tripped the earlier length or character guards, so the checksum branch was never actually exercised. Reimplement the ISO 6166 algorithm: require a numeric check digit, expand each letter to its two-digit value (A=10..Z=35), then run a right-to-left Luhn sum over the expanded digits. Validated against python-stdnum's isin.is_valid over 100k+ country-code-valid inputs with zero mismatches. Also dropped JP000K0VF054 from the valid samples (it is not a valid ISIN; it only passed because the checksum did nothing) and added wrong-check-digit cases to the invalid set.
1 parent 70de324 commit 474496d

2 files changed

Lines changed: 29 additions & 6 deletions

File tree

src/validators/finance.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,32 @@ def _cusip_checksum(cusip: str):
3232

3333

3434
def _isin_checksum(value: str):
35-
check, val = 0, None
35+
# The check digit (last character) is always numeric.
36+
if not value[-1].isdecimal():
37+
return False
3638

39+
# Expand the code into a string of digits, mapping each letter to its
40+
# two-digit value (A=10, ..., Z=35) and leaving digits unchanged.
41+
digits = ""
3742
for idx in range(12):
3843
c = value[idx]
3944
if c >= "0" and c <= "9" and idx > 1:
40-
val = ord(c) - ord("0")
45+
digits += c
4146
elif c >= "A" and c <= "Z":
42-
val = 10 + ord(c) - ord("A")
47+
digits += str(10 + ord(c) - ord("A"))
4348
elif c >= "a" and c <= "z":
44-
val = 10 + ord(c) - ord("a")
49+
digits += str(10 + ord(c) - ord("a"))
4550
else:
4651
return False
4752

53+
# Luhn checksum over the expanded digit string: starting from the
54+
# rightmost digit, double every second digit and sum the resulting digits.
55+
check = 0
56+
for idx, c in enumerate(reversed(digits)):
57+
val = ord(c) - ord("0")
4858
if idx & 1:
4959
val += val
60+
check += (val // 10) + (val % 10)
5061

5162
return (check % 10) == 0
5263

tests/test_finance.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,25 @@ def test_returns_failed_validation_on_invalid_cusip(value: str):
2424
# ==> ISIN <== #
2525

2626

27-
@pytest.mark.parametrize("value", ["US0004026250", "JP000K0VF054", "US0378331005"])
27+
@pytest.mark.parametrize("value", ["US0004026250", "US0378331005", "GB0002634946", "DE000BAY0017"])
2828
def test_returns_true_on_valid_isin(value: str):
2929
"""Test returns true on valid isin."""
3030
assert isin(value)
3131

3232

33-
@pytest.mark.parametrize("value", ["010378331005", "XCVF", "00^^^1234", "A000009"])
33+
@pytest.mark.parametrize(
34+
"value",
35+
[
36+
"010378331005",
37+
"XCVF",
38+
"00^^^1234",
39+
"A000009",
40+
# valid length and characters but wrong check digit
41+
"US0378331006",
42+
"US0004026251",
43+
"GB0002634947",
44+
],
45+
)
3446
def test_returns_failed_validation_on_invalid_isin(value: str):
3547
"""Test returns failed validation on invalid isin."""
3648
assert isinstance(isin(value), ValidationError)

0 commit comments

Comments
 (0)