Files
zugferd-service/tests/test_real_invoice_regressions.py
T
m3ta-chiron ccf8ad67fa
CI / test (push) Canceled after 0s
CI / docker (push) Canceled after 0s
feat(tests): fixture sweep, anonymized real-invoice regression corpus, CI
- test_fixture_sweep: extract+validate every fixture PDF on every run;
  known extractor/validator gaps are catalogued explicitly and the sweep
  fails on any deviation from that catalogue
- tests/fixtures/real/: anonymized XML from real production invoices that
  revealed bugs (verified to contain no real-world data); first entry
  locks in the FC-tax-number/basis-quantity/spaced-VAT-ID regressions
- CI workflow: uv sync --frozen from lockfile + pytest + docker build
2026-09-11 16:06:27 +02:00

85 lines
3.0 KiB
Python

"""Regression tests from real-world invoices (anonymized).
Every test in this module originated from a production bug. When a new real
invoice reveals a service bug: anonymize the invoice XML into
tests/fixtures/real/, verify it contains no real-world data, and add a test
here that locks in the correct behavior.
"""
from io import BytesIO
from pathlib import Path
import pytest
from pypdf import PdfWriter
from src.extractor import extract_zugferd
from src.models import ValidateRequest
from src.validator import validate_invoice
REAL_FIXTURES = Path(__file__).parent / "fixtures" / "real"
ALL_CHECKS = ["pflichtfelder", "betraege", "ustid", "pdf_abgleich"]
def _pdf_with_attachment(xml_bytes: bytes) -> bytes:
"""Wrap invoice XML in a minimal PDF (no visual content)."""
writer = PdfWriter()
writer.add_blank_page(width=72, height=72)
writer.add_attachment("factur-x.xml", xml_bytes)
output = BytesIO()
writer.write(output)
return output.getvalue()
class TestFcTaxNumberBasisQuantityInvoice:
"""Anonymized EXTENDED invoice: FC-only tax registration (no VAT ID),
unit price with basis quantity 100, spaced buyer VAT ID, seller email
in DefinedTradeContact.
Regressions locked in (found 2026-09 on a real workshop invoice):
supplier tax_number never extracted, unit price off by factor 100,
spaced VAT ID rejected as invalid format, contact email dropped.
"""
XML_PATH = REAL_FIXTURES / "extended_fc_steuernummer_basisqty.xml"
def test_extraction(self) -> None:
pdf_bytes = _pdf_with_attachment(self.XML_PATH.read_bytes())
result = extract_zugferd(pdf_bytes)
assert result.is_zugferd is True
data = result.xml_data
assert data.invoice_number == "4711"
assert data.supplier.tax_number == "1112223334444"
assert data.supplier.vat_id is None
assert data.supplier.email == "rechnung@beispielwerkstatt.example"
assert data.buyer.vat_id == "DE 123 456 789"
assert len(data.line_items) == 2
first, second = data.line_items
assert first.quantity == 140
assert first.unit_price == pytest.approx(0.7585)
assert first.line_total == pytest.approx(106.19)
assert second.quantity == 50
assert second.unit_price == pytest.approx(0.7585)
assert second.line_total == pytest.approx(37.93)
assert data.totals.net == pytest.approx(144.12)
assert data.totals.vat_total == pytest.approx(10.09)
assert data.totals.gross == pytest.approx(154.21)
def test_validation_all_checks_pass(self) -> None:
pdf_bytes = _pdf_with_attachment(self.XML_PATH.read_bytes())
result = extract_zugferd(pdf_bytes)
request = ValidateRequest(
xml_data=result.xml_data.model_dump(),
pdf_text=result.pdf_text,
checks=ALL_CHECKS,
)
validation = validate_invoice(request)
assert validation.is_valid is True
assert validation.errors == []