_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.
Problem
_isin_checksum never actually validates the check digit. It computes a per-character val inside the loop but never adds it to check:
So (check % 10) == 0 is always True and isin() accepts any 12-character string with valid characters, regardless of the check digit:
The existing "invalid" test cases (010378331005, XCVF, 00^^^1234, A000009) are all rejected earlier by the length / character / idx > 1 guards, so the checksum branch was effectively untested and the bug stayed hidden.
Fix
Implement the actual ISO 6166 algorithm: the check digit must be numeric, expand each letter to its two-digit value (A=10 .. Z=35), then run a right-to-left Luhn sum over the expanded digit string.
I validated the new implementation against python-stdnum's authoritative isin.is_valid over 100k+ country-code-valid random inputs plus random fuzzing: zero mismatches.
Tests
pytest tests/test_finance.py is green (25 passed); ruff check and ruff format --check clean.