"""USt-IdNr. (VAT ID) format validation for DE and common EU countries. Provides basic regex-based format validation. Does NOT perform live API verification. """ import re # Country-specific VAT ID regex patterns. # Each pattern validates the format after the 2-letter country code prefix. _VAT_PATTERNS: dict[str, re.Pattern[str]] = { # DE: DE + 9 digits (e.g. DE123456789) "DE": re.compile(r"^DE\d{9}$"), # AT: AT + U + 8 digits (e.g. ATU12345678) "AT": re.compile(r"^ATU\d{8}$"), # FR: FR + 2 alphanumeric + 9 digits (e.g. FRAB123456789) "FR": re.compile(r"^FR[A-Za-z0-9]{2}\d{9}$"), # NL: NL + 9 digits + B + 2 digits (e.g. NL123456789B01) "NL": re.compile(r"^NL\d{9}B\d{2}$"), # PL: PL + 10 digits (e.g. PL1234567890) "PL": re.compile(r"^PL\d{10}$"), # CZ: CZ + 8-10 digits (e.g. CZ1234567890) "CZ": re.compile(r"^CZ\d{8,10}$"), # IT: IT + 11 digits (e.g. IT12345678901) "IT": re.compile(r"^IT\d{11}$"), # ES: ES + 1 alphanumeric + 7 digits + 1 alphanumeric (e.g. ESA1234567B) "ES": re.compile(r"^ES[A-Za-z0-9]\d{7}[A-Za-z0-9]$"), # BE: BE + 10 digits (e.g. BE1234567890) "BE": re.compile(r"^BE\d{10}$"), # DK: DK + 8 digits (e.g. DK12345678) "DK": re.compile(r"^DK\d{8}$"), # SE: SE + 10 digits (e.g. SE1234567890) "SE": re.compile(r"^SE\d{10}$"), } # Fallback pattern for EU countries not explicitly listed above. # Accepts <2-letter country code> + 5-15 alphanumeric characters. _EU_FALLBACK_PATTERN = re.compile(r"^[A-Z]{2}[A-Za-z0-9]{5,15}$") # Set of supported EU country codes (ISO 3166-1 alpha-2). _EU_COUNTRY_CODES: set[str] = { "AT", "BE", "BG", "CY", "CZ", "DE", "DK", "EE", "ES", "FI", "FR", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL", "PT", "RO", "SE", "SI", "SK", } def get_country_code_from_vat_id(vat_id: str) -> str | None: """Extract the 2-letter country code from a VAT ID string. Returns None if the string is too short or does not start with letters. """ if not vat_id or len(vat_id) < 3: return None prefix = vat_id[:2].upper() if not prefix.isalpha(): return None return prefix def validate_vat_id(vat_id: str) -> bool: """Validate the format of a VAT ID (USt-IdNr.). Supports DE and common EU countries with specific regex patterns. For other EU countries, a fallback pattern is used. Non-EU or unrecognised formats return False. Args: vat_id: The VAT ID string to validate (case-insensitive, will be uppercased). Returns: True if the format is valid, False otherwise. """ if not vat_id: return True # Empty VAT ID is valid (nullable field) normalized = vat_id.strip().upper().replace(" ", "") country_code = get_country_code_from_vat_id(normalized) if country_code is None: return False # Check against country-specific pattern if available pattern = _VAT_PATTERNS.get(country_code) if pattern is not None: return bool(pattern.match(normalized)) # Fallback for EU countries without a specific pattern if country_code in _EU_COUNTRY_CODES: return bool(_EU_FALLBACK_PATTERN.match(normalized)) # Non-EU country code: reject (this module validates EU VAT IDs only) return False def validate_vat_id_or_raise(vat_id: str | None) -> str | None: """Validate VAT ID format and return the normalised value. Raises ValueError if the format is invalid. Args: vat_id: The VAT ID string to validate, or None. Returns: The normalised (uppercased, whitespace-stripped) VAT ID, or None. Raises: ValueError: If the VAT ID format is invalid. """ if vat_id is None or vat_id == "": return None normalized = vat_id.strip().upper().replace(" ", "") if not validate_vat_id(normalized): raise ValueError(f"Invalid VAT ID format: '{vat_id}'") return normalized