fix: extract tax number, price basis quantity, spaced VAT IDs and contact email

- supplier/buyer: extract FC tax registration (BT-32/BT-49) into new
  tax_number field; previously only VA (USt-IdNr.) was read
- pflichtfelder: critical error only when neither vat_id nor tax_number
  is present (EN 16931 BR-CO-26)
- ustid: tolerate formatting whitespace inside VAT IDs (e.g. 'DE 140 978 617')
- line items: normalize unit price by basis quantity (BT-149), so a
  price per 100 units no longer inflates line totals by factor 100
- supplier email: also look in DefinedTradeContact (BT-42)
- rewrite xrechnung fallback test to assert only our own behavior,
  independent of factur-x version specifics
This commit is contained in:
m3ta-chiron
2026-09-11 16:05:41 +02:00
parent e2e1d8c046
commit 7fcb3ff21d
7 changed files with 294 additions and 21 deletions
+3 -2
View File
@@ -109,6 +109,7 @@ Extract ZUGFeRD data from a base64-encoded PDF file.
"city": "Berlin", "city": "Berlin",
"country": "DE", "country": "DE",
"vat_id": "DE123456789", "vat_id": "DE123456789",
"tax_number": null,
"email": "billing@acme-corp.de" "email": "billing@acme-corp.de"
}, },
"buyer": { "buyer": {
@@ -276,7 +277,7 @@ The service supports four validation checks:
Validates that all critical invoice fields are present and non-empty: Validates that all critical invoice fields are present and non-empty:
- **Critical errors:** invoice_number, invoice_date, supplier.name, supplier.vat_id, buyer.name, totals.net, totals.gross, totals.vat_total, line_items array, line item fields - **Critical errors:** invoice_number, invoice_date, supplier.name, supplier.vat_id (or supplier.tax_number), buyer.name, totals.net, totals.gross, totals.vat_total, line_items array, line item fields
- **Warnings:** due_date, payment_terms.iban - **Warnings:** due_date, payment_terms.iban
### 2. betraege (Amount Calculations) ### 2. betraege (Amount Calculations)
@@ -470,7 +471,7 @@ PORT=8000 python -m src.main
Check that all required fields are present: Check that all required fields are present:
- invoice_number - invoice_number
- invoice_date (YYYY-MM-DD format) - invoice_date (YYYY-MM-DD format)
- supplier.name and supplier.vat_id - supplier.name and supplier.vat_id (or supplier.tax_number — EN 16931 BR-CO-26 requires one of both)
- buyer.name - buyer.name
- Non-zero totals (net, gross, vat_total) - Non-zero totals (net, gross, vat_total)
- At least one line item with description, quantity, and unit_price - At least one line item with description, quantity, and unit_price
+32 -1
View File
@@ -104,11 +104,23 @@ def parse_supplier(xml_root: etree._Element) -> Supplier:
"ram:ID[@schemeID='VA']/text()", "ram:ID[@schemeID='VA']/text()",
namespaces=NAMESPACES, namespaces=NAMESPACES,
) )
tax_number = xml_root.xpath(
"//ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:SpecifiedTaxRegistration/"
"ram:ID[@schemeID='FC']/text()",
namespaces=NAMESPACES,
)
email = xml_root.xpath( email = xml_root.xpath(
"//ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/" "//ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/"
"ram:URIUniversalCommunication/ram:URIID/text()", "ram:URIUniversalCommunication/ram:URIID/text()",
namespaces=NAMESPACES, namespaces=NAMESPACES,
) )
# Fallback: email address from the seller's defined trade contact (BT-42)
if not email:
email = xml_root.xpath(
"//ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/"
"ram:DefinedTradeContact/ram:EmailURIUniversalCommunication/ram:URIID/text()",
namespaces=NAMESPACES,
)
return Supplier( return Supplier(
name=name[0] if name else "", name=name[0] if name else "",
@@ -117,6 +129,7 @@ def parse_supplier(xml_root: etree._Element) -> Supplier:
city=city[0] if city else None, city=city[0] if city else None,
country=country[0] if country else None, country=country[0] if country else None,
vat_id=vat_id[0] if vat_id else None, vat_id=vat_id[0] if vat_id else None,
tax_number=tax_number[0] if tax_number else None,
email=email[0] if email else None, email=email[0] if email else None,
) )
@@ -159,6 +172,11 @@ def parse_buyer(xml_root: etree._Element) -> Buyer:
"ram:ID[@schemeID='VA']/text()", "ram:ID[@schemeID='VA']/text()",
namespaces=NAMESPACES, namespaces=NAMESPACES,
) )
tax_number = xml_root.xpath(
"//ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty/ram:SpecifiedTaxRegistration/"
"ram:ID[@schemeID='FC']/text()",
namespaces=NAMESPACES,
)
return Buyer( return Buyer(
name=name[0] if name else "", name=name[0] if name else "",
@@ -167,6 +185,7 @@ def parse_buyer(xml_root: etree._Element) -> Buyer:
city=city[0] if city else None, city=city[0] if city else None,
country=country[0] if country else None, country=country[0] if country else None,
vat_id=vat_id[0] if vat_id else None, vat_id=vat_id[0] if vat_id else None,
tax_number=tax_number[0] if tax_number else None,
) )
@@ -209,6 +228,11 @@ def parse_line_items(xml_root: etree._Element) -> list[LineItem]:
"./ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount/text()", "./ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount/text()",
namespaces=NAMESPACES, namespaces=NAMESPACES,
) )
basis_quantity = item_node.xpath(
"./ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/"
"ram:BasisQuantity/text()",
namespaces=NAMESPACES,
)
line_total = item_node.xpath( line_total = item_node.xpath(
"./ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation/" "./ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation/"
"ram:LineTotalAmount/text()", "ram:LineTotalAmount/text()",
@@ -225,6 +249,13 @@ def parse_line_items(xml_root: etree._Element) -> list[LineItem]:
unit = unit_code[0] if unit_code else "Stück" unit = unit_code[0] if unit_code else "Stück"
# Basis quantity (BT-149): charge amount applies to N units (e.g. price per 100)
effective_unit_price = float(unit_price[0]) if unit_price else None
if effective_unit_price is not None and basis_quantity:
base = float(basis_quantity[0])
if base > 0:
effective_unit_price = effective_unit_price / base
items.append( items.append(
LineItem( LineItem(
position=position, position=position,
@@ -235,7 +266,7 @@ def parse_line_items(xml_root: etree._Element) -> list[LineItem]:
description=description[0] if description else "", description=description[0] if description else "",
quantity=float(quantity[0]) if quantity else 0.0, quantity=float(quantity[0]) if quantity else 0.0,
unit=unit, unit=unit,
unit_price=float(unit_price[0]) if unit_price else None, unit_price=effective_unit_price,
line_total=float(line_total[0]) if line_total else None, line_total=float(line_total[0]) if line_total else None,
vat_rate=float(vat_rate[0]) if vat_rate else None, vat_rate=float(vat_rate[0]) if vat_rate else None,
vat_amount=float(vat_amount[0]) if vat_amount else None, vat_amount=float(vat_amount[0]) if vat_amount else None,
+6
View File
@@ -24,6 +24,9 @@ class Supplier(BaseModel):
city: str | None = Field(default=None, description="City") city: str | None = Field(default=None, description="City")
country: str | None = Field(default=None, description="Country code") country: str | None = Field(default=None, description="Country code")
vat_id: str | None = Field(default=None, description="VAT ID") vat_id: str | None = Field(default=None, description="VAT ID")
tax_number: str | None = Field(
default=None, description="Tax number (Steuernummer, BT-32)"
)
email: str | None = Field(default=None, description="Email address") email: str | None = Field(default=None, description="Email address")
@@ -36,6 +39,9 @@ class Buyer(BaseModel):
city: str | None = Field(default=None, description="City") city: str | None = Field(default=None, description="City")
country: str | None = Field(default=None, description="Country code") country: str | None = Field(default=None, description="Country code")
vat_id: str | None = Field(default=None, description="VAT ID") vat_id: str | None = Field(default=None, description="VAT ID")
tax_number: str | None = Field(
default=None, description="Tax number (Steuernummer, BT-49)"
)
class VatBreakdown(BaseModel): class VatBreakdown(BaseModel):
+18 -5
View File
@@ -19,13 +19,13 @@ def validate_pflichtfelder(xml_data: XmlData) -> list[ErrorDetail]:
"""Check required fields are present.""" """Check required fields are present."""
errors = [] errors = []
def add_error(field: str, severity: str) -> None: def add_error(field: str, severity: str, message: str | None = None) -> None:
errors.append( errors.append(
ErrorDetail( ErrorDetail(
check="pflichtfelder", check="pflichtfelder",
field=field, field=field,
error_code="missing_required", error_code="missing_required",
message=f"Required field '{field}' is missing or empty", message=message or f"Required field '{field}' is missing or empty",
severity=severity, severity=severity,
) )
) )
@@ -40,8 +40,20 @@ def validate_pflichtfelder(xml_data: XmlData) -> list[ErrorDetail]:
if not xml_data.supplier.name or not xml_data.supplier.name.strip(): if not xml_data.supplier.name or not xml_data.supplier.name.strip():
add_error("supplier.name", "critical") add_error("supplier.name", "critical")
if not xml_data.supplier.vat_id or not xml_data.supplier.vat_id.strip(): # EN 16931 BR-CO-26: seller identified by VAT ID (BT-31) OR tax number (BT-32)
add_error("supplier.vat_id", "critical") has_vat_id = bool(xml_data.supplier.vat_id and xml_data.supplier.vat_id.strip())
has_tax_number = bool(
xml_data.supplier.tax_number and xml_data.supplier.tax_number.strip()
)
if not has_vat_id and not has_tax_number:
add_error(
"supplier.vat_id",
"critical",
message=(
"Supplier must have a VAT ID (supplier.vat_id) "
"or a tax number (supplier.tax_number)"
),
)
if not xml_data.buyer.name or not xml_data.buyer.name.strip(): if not xml_data.buyer.name or not xml_data.buyer.name.strip():
add_error("buyer.name", "critical") add_error("buyer.name", "critical")
@@ -163,7 +175,8 @@ def validate_ustid(vat_id: str) -> ErrorDetail | None:
severity="critical", severity="critical",
) )
vat_id = vat_id.strip() # Whitespace inside VAT IDs is formatting only (e.g. "DE 140 978 617")
vat_id = re.sub(r"\s+", "", vat_id)
# German VAT ID: DE followed by 9 digits # German VAT ID: DE followed by 9 digits
if vat_id.startswith("DE"): if vat_id.startswith("DE"):
+173 -13
View File
@@ -307,12 +307,11 @@ class TestXRechnungExtraction:
"""Test extraction from XRechnung PDFs with non-standard filenames.""" """Test extraction from XRechnung PDFs with non-standard filenames."""
def test_xrechnung_by_xml_extension_fallback(self): def test_xrechnung_by_xml_extension_fallback(self):
"""Test XRechnung with 'xrechnung.xml' filename is detected via fallback. """Test XRechnung with 'xrechnung.xml' filename is extracted correctly.
This test verifies the fallback mechanism when factur-x library Depending on the factur-x version, the library recognizes non-standard
doesn't recognize non-standard filenames like 'xrechnung.xml'. attachment names itself or our fallback kicks in — both paths must
We create a synthetic PDF with a non-standard attachment name to yield the same result, so we only assert our own behavior here.
exercise the fallback code path.
""" """
from io import BytesIO from io import BytesIO
@@ -337,20 +336,14 @@ class TestXRechnungExtraction:
pdf_writer.write(output) pdf_writer.write(output)
pdf_bytes = output.getvalue() pdf_bytes = output.getvalue()
# Verify factur-x does NOT recognize it (returns False/None) # Verify our fallback function finds it on its own
from facturx import get_xml_from_pdf
facturx_result = get_xml_from_pdf(pdf_bytes, check_xsd=False)
assert facturx_result[0] is False, "factur-x should not recognize xrechnung.xml"
# Verify our fallback function DOES find it
fallback_result = _find_xml_attachment_fallback(pdf_bytes) fallback_result = _find_xml_attachment_fallback(pdf_bytes)
assert fallback_result[0] == "xrechnung.xml", ( assert fallback_result[0] == "xrechnung.xml", (
"Fallback should find xrechnung.xml" "Fallback should find xrechnung.xml"
) )
assert fallback_result[1] is not None, "Fallback should return XML content" assert fallback_result[1] is not None, "Fallback should return XML content"
# Verify full extraction works via fallback # Verify full extraction works, whichever path finds the XML
result = extract_zugferd(pdf_bytes) result = extract_zugferd(pdf_bytes)
assert result.is_zugferd is True assert result.is_zugferd is True
@@ -358,3 +351,170 @@ class TestXRechnungExtraction:
assert result.xml_data is not None assert result.xml_data is not None
assert result.xml_data.invoice_number is not None assert result.xml_data.invoice_number is not None
assert result.extraction_meta.xml_attachment_name == "xrechnung.xml" assert result.extraction_meta.xml_attachment_name == "xrechnung.xml"
TAX_REGISTRATION_XML_TEMPLATE = """<rsm:CrossIndustryInvoice
xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100">
<rsm:SupplyChainTradeTransaction>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>Test Seller GmbH</ram:Name>
{seller_tax_registrations}
{seller_contact}
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>Test Buyer AG</ram:Name>
{buyer_tax_registrations}
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>"""
VA_REGISTRATION = (
"<ram:SpecifiedTaxRegistration>"
'<ram:ID schemeID="VA">DE123456789</ram:ID>'
"</ram:SpecifiedTaxRegistration>"
)
FC_REGISTRATION = (
"<ram:SpecifiedTaxRegistration>"
'<ram:ID schemeID="FC">2281410000697</ram:ID>'
"</ram:SpecifiedTaxRegistration>"
)
BUYER_FC_REGISTRATION = (
"<ram:SpecifiedTaxRegistration>"
'<ram:ID schemeID="FC">9876543210</ram:ID>'
"</ram:SpecifiedTaxRegistration>"
)
CONTACT_WITH_EMAIL = (
"<ram:DefinedTradeContact>"
"<ram:EmailURIUniversalCommunication>"
"<ram:URIID>seller@example.de</ram:URIID>"
"</ram:EmailURIUniversalCommunication>"
"</ram:DefinedTradeContact>"
)
class TestTaxRegistrationExtraction:
"""Test extraction of tax registrations (VAT ID vs. tax number)."""
def _parse(self, seller_regs: str, buyer_regs: str, seller_contact: str = ""):
from lxml import etree
from src.extractor import parse_buyer, parse_supplier
xml = TAX_REGISTRATION_XML_TEMPLATE.format(
seller_tax_registrations=seller_regs,
buyer_tax_registrations=buyer_regs,
seller_contact=seller_contact,
)
root = etree.fromstring(xml.encode("utf-8"))
return parse_supplier(root), parse_buyer(root)
def test_seller_vat_id_and_tax_number(self):
"""Seller with VA and FC registrations extracts both fields."""
supplier, _ = self._parse(VA_REGISTRATION + FC_REGISTRATION, "")
assert supplier.vat_id == "DE123456789"
assert supplier.tax_number == "2281410000697"
def test_seller_tax_number_only(self):
"""Seller with only FC registration (e.g. Kleinunternehmer/Werkstatt)."""
supplier, _ = self._parse(FC_REGISTRATION, "")
assert supplier.vat_id is None
assert supplier.tax_number == "2281410000697"
def test_seller_vat_id_only(self):
"""Seller with only VA registration leaves tax_number empty."""
supplier, _ = self._parse(VA_REGISTRATION, "")
assert supplier.vat_id == "DE123456789"
assert supplier.tax_number is None
def test_buyer_tax_number(self):
"""Buyer with FC registration extracts tax_number."""
_, buyer = self._parse(VA_REGISTRATION, BUYER_FC_REGISTRATION)
assert buyer.vat_id is None
assert buyer.tax_number == "9876543210"
def test_seller_email_from_trade_contact(self):
"""Seller email is found in DefinedTradeContact (BT-42)."""
supplier, _ = self._parse(
VA_REGISTRATION, "", seller_contact=CONTACT_WITH_EMAIL
)
assert supplier.email == "seller@example.de"
def test_seller_email_absent(self):
"""No email elements leaves email as None."""
supplier, _ = self._parse(VA_REGISTRATION, "")
assert supplier.email is None
LINE_ITEM_XML_TEMPLATE = """<rsm:CrossIndustryInvoice
xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100">
<rsm:SupplyChainTradeTransaction>
<ram:IncludedSupplyChainTradeLineItem>
<ram:SpecifiedTradeProduct>
<ram:Name>Flanschsicherungsset</ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>{charge_amount}</ram:ChargeAmount>
{basis_quantity}
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">{billed_quantity}</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>{line_total}</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>"""
class TestBasisQuantityExtraction:
"""Test unit price normalization via BasisQuantity (BT-149)."""
def _parse_items(self, basis_quantity: str):
from lxml import etree
from src.extractor import parse_line_items
xml = LINE_ITEM_XML_TEMPLATE.format(
charge_amount="75.85",
basis_quantity=basis_quantity,
billed_quantity="140",
line_total="106.19",
)
root = etree.fromstring(xml.encode("utf-8"))
return parse_line_items(root)
def test_unit_price_divided_by_basis_quantity(self):
"""Charge amount per 100 units is normalized to per-unit price."""
items = self._parse_items("<ram:BasisQuantity>100</ram:BasisQuantity>")
assert len(items) == 1
assert items[0].unit_price == pytest.approx(0.7585)
def test_unit_price_without_basis_quantity(self):
"""Without BasisQuantity the charge amount stays as-is."""
items = self._parse_items("")
assert len(items) == 1
assert items[0].unit_price == pytest.approx(75.85)
def test_unit_price_with_zero_basis_quantity(self):
"""Zero BasisQuantity is ignored (division guard)."""
items = self._parse_items("<ram:BasisQuantity>0</ram:BasisQuantity>")
assert len(items) == 1
assert items[0].unit_price == pytest.approx(75.85)
+5
View File
@@ -40,6 +40,7 @@ class TestSupplier:
assert supplier.name == "ACME GmbH" assert supplier.name == "ACME GmbH"
assert supplier.street is None assert supplier.street is None
assert supplier.vat_id is None assert supplier.vat_id is None
assert supplier.tax_number is None
def test_full_supplier(self): def test_full_supplier(self):
"""Test Supplier with all fields.""" """Test Supplier with all fields."""
@@ -52,6 +53,7 @@ class TestSupplier:
city="Musterstadt", city="Musterstadt",
country="DE", country="DE",
vat_id="DE123456789", vat_id="DE123456789",
tax_number="2281410000697",
email="info@acme.de", email="info@acme.de",
) )
assert supplier.name == "ACME GmbH" assert supplier.name == "ACME GmbH"
@@ -60,6 +62,7 @@ class TestSupplier:
assert supplier.city == "Musterstadt" assert supplier.city == "Musterstadt"
assert supplier.country == "DE" assert supplier.country == "DE"
assert supplier.vat_id == "DE123456789" assert supplier.vat_id == "DE123456789"
assert supplier.tax_number == "2281410000697"
assert supplier.email == "info@acme.de" assert supplier.email == "info@acme.de"
@@ -85,10 +88,12 @@ class TestBuyer:
city="Kundenstadt", city="Kundenstadt",
country="DE", country="DE",
vat_id="DE987654321", vat_id="DE987654321",
tax_number="1234567890",
) )
assert buyer.name == "Customer AG" assert buyer.name == "Customer AG"
assert buyer.street == "Kundenweg 7" assert buyer.street == "Kundenweg 7"
assert buyer.vat_id == "DE987654321" assert buyer.vat_id == "DE987654321"
assert buyer.tax_number == "1234567890"
class TestVatBreakdown: class TestVatBreakdown:
+57
View File
@@ -149,6 +149,58 @@ class TestValidatePflichtfelder:
assert any(e.field == "supplier.vat_id" for e in errors) assert any(e.field == "supplier.vat_id" for e in errors)
assert any(e.severity == "critical" for e in errors) assert any(e.severity == "critical" for e in errors)
def test_tax_number_only_supplier_no_critical_error(self):
"""Supplier with only a tax number (no VAT ID) should pass BR-CO-26."""
xml_data = XmlData(
invoice_number="INV001",
invoice_date="2024-01-15",
supplier=Supplier(
name="Werkstatt gGmbH",
vat_id=None,
tax_number="2281410000697",
),
buyer=Buyer(name="Test Buyer AG"),
line_items=[
LineItem(
position=1,
description="Test Product",
quantity=2.0,
unit="HUR",
unit_price=5.0,
line_total=10.0,
vat_rate=7.0,
)
],
totals=Totals(line_total_sum=10.0, net=10.0, vat_total=0.7, gross=10.7),
)
errors = validate_pflichtfelder(xml_data)
assert not any(e.field == "supplier.vat_id" for e in errors)
def test_missing_vat_id_and_tax_number_critical(self):
"""Supplier with neither VAT ID nor tax number stays critical."""
xml_data = XmlData(
invoice_number="INV001",
invoice_date="2024-01-15",
supplier=Supplier(name="Test Supplier GmbH", vat_id="", tax_number=""),
buyer=Buyer(name="Test Buyer AG"),
line_items=[
LineItem(
position=1,
description="Test Product",
quantity=2.0,
unit="HUR",
unit_price=5.0,
line_total=10.0,
vat_rate=7.0,
)
],
totals=Totals(line_total_sum=10.0, net=10.0, vat_total=0.7, gross=10.7),
)
errors = validate_pflichtfelder(xml_data)
assert any(e.field == "supplier.vat_id" for e in errors)
assert any(e.severity == "critical" for e in errors)
assert any("tax number" in e.message for e in errors)
def test_missing_buyer_name_critical(self): def test_missing_buyer_name_critical(self):
"""Missing buyer.name should produce critical error.""" """Missing buyer.name should produce critical error."""
xml_data = XmlData( xml_data = XmlData(
@@ -720,6 +772,11 @@ class TestValidateUstid:
error = validate_ustid("DE123456789") error = validate_ustid("DE123456789")
assert error is None assert error is None
def test_valid_german_vat_id_with_spaces(self):
"""German VAT ID with formatting spaces (e.g. 'DE 140 978 617') passes."""
error = validate_ustid("DE 140 978 617")
assert error is None
def test_valid_austrian_vat_id(self): def test_valid_austrian_vat_id(self):
"""Valid Austrian VAT ID should pass.""" """Valid Austrian VAT ID should pass."""
error = validate_ustid("ATU12345678") error = validate_ustid("ATU12345678")