139 lines
4.0 KiB
Python
139 lines
4.0 KiB
Python
"""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
|