From 26a63ebe300cbf45cc7249c2f07586b113ba22f4 Mon Sep 17 00:00:00 2001 From: Cedric Conday Date: Tue, 30 Jun 2026 03:37:13 +0000 Subject: [PATCH] vatin: reject a duplicated country code prefix (#420) vatin.validate() stripped the leading country code itself and then passed the remainder to the country module, which strips its own optional country code prefix again. For a doubled prefix such as 'BE BE 0308.357.159' both strips fired, leaving a valid national number, so the VATIN validated even though stdnum.eu.vat correctly rejects it. Validate the full number with the country module (which strips its own prefix once), mirroring stdnum.eu.vat, and only fall back to stripping the country code for modules that do not recognise it - guarding that fallback so a doubled prefix is not stripped a second time. Closes #420. --- stdnum/vatin.py | 21 ++++++++++++++++++--- tests/test_vatin.doctest | 16 ++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/stdnum/vatin.py b/stdnum/vatin.py index 3e7e2f56..1ef6743c 100644 --- a/stdnum/vatin.py +++ b/stdnum/vatin.py @@ -87,11 +87,26 @@ def validate(number: str) -> str: This performs the country-specific check for the number. """ number = clean(number, '').strip() - module = _get_cc_module(number[:2]) + cc = number[:2] + module = _get_cc_module(cc) try: - return number[:2].upper() + module.validate(number[2:]) + # Most country modules accept and strip the optional country-code + # prefix themselves, so the full number is validated. Stripping the + # prefix here as well would silently accept a doubled country code + # such as "BE BE 0308.357.159" (see #420). + result = module.validate(number) except ValidationError: - return module.validate(number) + # Some country modules expect the national number without the country + # code prefix. Only retry that way when the remainder is not itself + # prefixed with the country code, otherwise a doubled prefix would be + # accepted. + remainder = re.sub(r'[^0-9A-Za-z]', '', number[2:]) + if remainder[:2].upper() == cc.upper(): + raise + result = module.validate(number[2:]) + if not result.startswith(cc.upper()): + result = cc.upper() + result + return result def is_valid(number: str) -> bool: diff --git a/tests/test_vatin.doctest b/tests/test_vatin.doctest index 1c65db3e..30bd3cc6 100644 --- a/tests/test_vatin.doctest +++ b/tests/test_vatin.doctest @@ -102,3 +102,19 @@ Check for VAT numbers that cannot be compacted without EU prefix: True >>> vatin.compact('EU191849184') 'EU191849184' + + +A duplicated country code prefix should not be accepted (#420). This used to +pass because the country code was stripped twice (once here and once by the +country module): + +>>> vatin.is_valid('BE 0308.357.159') +True +>>> vatin.is_valid('BE BE 0308.357.159') +False +>>> vatin.is_valid('BEBE0308357159') +False +>>> vatin.validate('BE BE 0308.357.159') +Traceback (most recent call last): + ... +InvalidFormat: ...