From 207861483d75a10370d0946d909643f294fd34d6 Mon Sep 17 00:00:00 2001 From: Triskell Studio Date: Wed, 24 Jun 2026 15:14:32 +0200 Subject: [PATCH] fix(finance): isin() actually validates the check digit _isin_checksum never accumulated into `check` (the accumulation line present in _cusip_checksum was missing), and the per-character loop it copied is not the ISIN scheme anyway. `check` stayed 0, so isin() returned True for every 12-character string -- e.g. isin('US0378331004') (wrong check digit) and isin('XX0000000000'). Reimplement with the documented ISIN algorithm: expand letters to digits (A=10..Z=35) and run Luhn over the result. Verified against real ISINs (US0378331005, AU0000XVGZA3, GB0002634946). The existing 'valid' fixture 'JP000K0VF054' was itself invalid (its correct check digit is 5) -- corrected to 'JP000K0VF055'. Added a wrong-check-digit case to the invalid fixtures and a valid example to the docstring. --- src/validators/finance.py | 30 ++++++++++++++++++------------ tests/test_finance.py | 4 ++-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/validators/finance.py b/src/validators/finance.py index 9df5a970..2f5e71e6 100644 --- a/src/validators/finance.py +++ b/src/validators/finance.py @@ -32,21 +32,27 @@ def _cusip_checksum(cusip: str): def _isin_checksum(value: str): - check, val = 0, None + if not (value[:2].isalpha() and value[-1].isdigit()): + return False - for idx in range(12): - c = value[idx] - if c >= "0" and c <= "9" and idx > 1: - val = ord(c) - ord("0") - elif c >= "A" and c <= "Z": - val = 10 + ord(c) - ord("A") - elif c >= "a" and c <= "z": - val = 10 + ord(c) - ord("a") + # Expand letters to numbers (A=10, ..., Z=35), then run the Luhn algorithm. + digits = "" + for char in value: + if char.isdigit(): + digits += char + elif char.isalpha(): + digits += str(10 + ord(char.upper()) - ord("A")) else: return False + check = 0 + for idx, digit in enumerate(reversed(digits)): + val = int(digit) if idx & 1: - val += val + val *= 2 + if val > 9: + val -= 9 + check += val return (check % 10) == 0 @@ -82,8 +88,8 @@ def isin(value: str): [1]: https://en.wikipedia.org/wiki/International_Securities_Identification_Number Examples: - >>> isin('037833DP2') - ValidationError(func=isin, args={'value': '037833DP2'}) + >>> isin('US0378331005') + True >>> isin('037833DP3') ValidationError(func=isin, args={'value': '037833DP3'}) diff --git a/tests/test_finance.py b/tests/test_finance.py index a40fd333..90cae2be 100644 --- a/tests/test_finance.py +++ b/tests/test_finance.py @@ -24,13 +24,13 @@ def test_returns_failed_validation_on_invalid_cusip(value: str): # ==> ISIN <== # -@pytest.mark.parametrize("value", ["US0004026250", "JP000K0VF054", "US0378331005"]) +@pytest.mark.parametrize("value", ["US0004026250", "JP000K0VF055", "US0378331005"]) def test_returns_true_on_valid_isin(value: str): """Test returns true on valid isin.""" assert isin(value) -@pytest.mark.parametrize("value", ["010378331005", "XCVF", "00^^^1234", "A000009"]) +@pytest.mark.parametrize("value", ["010378331005", "XCVF", "00^^^1234", "A000009", "US0378331004"]) def test_returns_failed_validation_on_invalid_isin(value: str): """Test returns failed validation on invalid isin.""" assert isinstance(isin(value), ValidationError)