"""Tests for ZUGFeRD validator using TDD approach.""" import pytest from src.models import ( XmlData, Supplier, Buyer, LineItem, Totals, VatBreakdown, PaymentTerms, ErrorDetail, ValidationResult, ValidateRequest, ) from src.validator import ( validate_pflichtfelder, validate_betraege, validate_ustid, validate_pdf_abgleich, validate_invoice, ) class TestValidatePflichtfelder: """Tests for validate_pflichtfelder function.""" def test_complete_data_no_errors(self): """Complete data should pass validation.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", due_date="2024-02-15", supplier=Supplier( name="Test Supplier GmbH", vat_id="DE123456789", ), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Test Product", quantity=10.0, unit="Stück", unit_price=99.99, line_total=999.90, vat_rate=19.0, ) ], totals=Totals( line_total_sum=999.90, net=999.90, vat_total=189.98, gross=1189.88, vat_breakdown=[VatBreakdown(rate=19.0, base=999.90, amount=189.98)], ), payment_terms=PaymentTerms(iban="DE89370400440532013000"), ) errors = validate_pflichtfelder(xml_data) assert len(errors) == 0 def test_missing_invoice_number_critical(self): """Missing invoice_number should produce critical error.""" xml_data = XmlData( invoice_number="", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Test Product", quantity=1.0, unit="Stück", unit_price=10.0, line_total=10.0, ) ], totals=Totals(line_total_sum=10.0, net=10.0, vat_total=0.0, gross=10.0), ) errors = validate_pflichtfelder(xml_data) assert any(e.field == "invoice_number" for e in errors) assert any(e.severity == "critical" for e in errors) assert any(e.error_code == "missing_required" for e in errors) def test_missing_invoice_date_critical(self): """Missing invoice_date should produce critical error.""" xml_data = XmlData( invoice_number="INV001", invoice_date="", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Test Product", quantity=1.0, unit="Stück", unit_price=10.0, line_total=10.0, ) ], totals=Totals(line_total_sum=10.0, net=10.0, vat_total=0.0, gross=10.0), ) errors = validate_pflichtfelder(xml_data) assert any(e.field == "invoice_date" for e in errors) assert any(e.severity == "critical" for e in errors) def test_missing_supplier_name_critical(self): """Missing supplier.name should produce critical error.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Test Product", quantity=1.0, unit="Stück", unit_price=10.0, line_total=10.0, ) ], totals=Totals(line_total_sum=10.0, net=10.0, vat_total=0.0, gross=10.0), ) errors = validate_pflichtfelder(xml_data) assert any(e.field == "supplier.name" for e in errors) assert any(e.severity == "critical" for e in errors) def test_missing_supplier_vat_id_critical(self): """Missing supplier.vat_id should produce critical error.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id=""), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Test Product", quantity=1.0, unit="Stück", unit_price=10.0, line_total=10.0, ) ], totals=Totals(line_total_sum=10.0, net=10.0, vat_total=0.0, gross=10.0), ) 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) def test_missing_buyer_name_critical(self): """Missing buyer.name should produce critical error.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name=""), line_items=[ LineItem( position=1, description="Test Product", quantity=1.0, unit="Stück", unit_price=10.0, line_total=10.0, ) ], totals=Totals(line_total_sum=10.0, net=10.0, vat_total=0.0, gross=10.0), ) errors = validate_pflichtfelder(xml_data) assert any(e.field == "buyer.name" for e in errors) assert any(e.severity == "critical" for e in errors) def test_missing_totals_net_critical(self): """Missing totals.net should produce critical error.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Test Product", quantity=1.0, unit="Stück", unit_price=10.0, line_total=10.0, ) ], totals=Totals(line_total_sum=10.0, net=0.0, vat_total=0.0, gross=10.0), ) errors = validate_pflichtfelder(xml_data) assert any(e.field == "totals.net" for e in errors) assert any(e.severity == "critical" for e in errors) def test_missing_totals_gross_critical(self): """Missing totals.gross should produce critical error.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Test Product", quantity=1.0, unit="Stück", unit_price=10.0, line_total=10.0, ) ], totals=Totals(line_total_sum=10.0, net=10.0, vat_total=0.0, gross=0.0), ) errors = validate_pflichtfelder(xml_data) assert any(e.field == "totals.gross" for e in errors) assert any(e.severity == "critical" for e in errors) def test_missing_totals_vat_total_critical(self): """Missing totals.vat_total should produce critical error.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Test Product", quantity=1.0, unit="Stück", unit_price=10.0, line_total=10.0, ) ], totals=Totals(line_total_sum=10.0, net=10.0, vat_total=0.0, gross=10.0), ) errors = validate_pflichtfelder(xml_data) assert any(e.field == "totals.vat_total" for e in errors) assert any(e.severity == "critical" for e in errors) def test_missing_due_date_warning(self): """Missing due_date should produce warning, not critical error.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", due_date="", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Test Product", quantity=1.0, unit="Stück", unit_price=10.0, line_total=10.0, ) ], totals=Totals(line_total_sum=10.0, net=10.0, vat_total=0.0, gross=10.0), ) errors = validate_pflichtfelder(xml_data) assert any(e.field == "due_date" for e in errors) assert any(e.severity == "warning" for e in errors) def test_missing_payment_iban_warning(self): """Missing payment_terms.iban should produce warning.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Test Product", quantity=1.0, unit="Stück", unit_price=10.0, line_total=10.0, ) ], totals=Totals(line_total_sum=10.0, net=10.0, vat_total=0.0, gross=10.0), payment_terms=PaymentTerms(iban=""), ) errors = validate_pflichtfelder(xml_data) assert any(e.field == "payment_terms.iban" for e in errors) assert any(e.severity == "warning" for e in errors) def test_missing_line_items_critical(self): """Empty line_items should produce critical error.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[], totals=Totals(line_total_sum=0.0, net=0.0, vat_total=0.0, gross=0.0), ) errors = validate_pflichtfelder(xml_data) assert any(e.field == "line_items" for e in errors) assert any(e.severity == "critical" for e in errors) def test_missing_line_item_description_critical(self): """Missing line_item.description should produce critical error.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="", quantity=1.0, unit="Stück", unit_price=10.0, line_total=10.0, ) ], totals=Totals(line_total_sum=10.0, net=10.0, vat_total=0.0, gross=10.0), ) errors = validate_pflichtfelder(xml_data) assert any(e.field == "line_items[0].description" for e in errors) assert any(e.severity == "critical" for e in errors) def test_missing_line_item_quantity_critical(self): """Missing line_item.quantity (zero) should produce critical error.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Test Product", quantity=0.0, unit="Stück", unit_price=10.0, line_total=10.0, ) ], totals=Totals(line_total_sum=10.0, net=10.0, vat_total=0.0, gross=10.0), ) errors = validate_pflichtfelder(xml_data) assert any(e.field == "line_items[0].quantity" for e in errors) assert any(e.severity == "critical" for e in errors) def test_missing_line_item_unit_price_critical(self): """Missing line_item.unit_price (zero) should produce critical error.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Test Product", quantity=1.0, unit="Stück", unit_price=0.0, line_total=10.0, ) ], totals=Totals(line_total_sum=10.0, net=10.0, vat_total=0.0, gross=10.0), ) errors = validate_pflichtfelder(xml_data) assert any(e.field == "line_items[0].unit_price" for e in errors) assert any(e.severity == "critical" for e in errors) def test_missing_line_item_line_total_critical(self): """Missing line_item.line_total (zero) should produce critical error.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Test Product", quantity=1.0, unit="Stück", unit_price=10.0, line_total=0.0, ) ], totals=Totals(line_total_sum=10.0, net=10.0, vat_total=0.0, gross=10.0), ) errors = validate_pflichtfelder(xml_data) assert any(e.field == "line_items[0].line_total" for e in errors) assert any(e.severity == "critical" for e in errors) def test_missing_line_item_vat_rate_warning(self): """Missing line_item.vat_rate should produce warning.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Test Product", quantity=1.0, unit="Stück", unit_price=10.0, line_total=10.0, ) ], totals=Totals(line_total_sum=10.0, net=10.0, vat_total=0.0, gross=10.0), ) errors = validate_pflichtfelder(xml_data) assert any(e.field == "line_items[0].vat_rate" for e in errors) assert any(e.severity == "warning" for e in errors) def test_multiple_critical_errors(self): """Multiple missing critical fields should all be reported.""" xml_data = XmlData( invoice_number="", invoice_date="", supplier=Supplier(name="", vat_id=""), buyer=Buyer(name=""), line_items=[], totals=Totals(line_total_sum=0.0, net=0.0, vat_total=0.0, gross=0.0), ) errors = validate_pflichtfelder(xml_data) critical_errors = [e for e in errors if e.severity == "critical"] assert len(critical_errors) > 0 # Should include at least: invoice_number, invoice_date, supplier.name, etc. class TestValidateBetraege: """Tests for validate_betraege function.""" def test_correct_calculations_no_errors(self): """Correct calculations should pass validation.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product A", quantity=10.0, unit="Stück", unit_price=9.99, line_total=99.90, vat_rate=19.0, ), LineItem( position=2, description="Product B", quantity=5.0, unit="Stück", unit_price=19.99, line_total=99.95, vat_rate=19.0, ), ], totals=Totals( line_total_sum=199.85, net=199.85, vat_total=37.97, gross=237.82, vat_breakdown=[VatBreakdown(rate=19.0, base=199.85, amount=37.97)], ), ) errors = validate_betraege(xml_data) assert len(errors) == 0 def test_line_total_mismatch(self): """line_total should equal quantity × unit_price.""" line = LineItem( position=1, description="Test Product", quantity=10, unit="Stück", unit_price=9.99, line_total=100.00, # Wrong: 10 × 9.99 = 99.90 ) xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[line], totals=Totals(line_total_sum=100.0, net=100.0, vat_total=0.0, gross=100.0), ) errors = validate_betraege(xml_data) assert any(e.error_code == "calculation_mismatch" for e in errors) assert any( "line_total" in e.field for e in errors if e.error_code == "calculation_mismatch" ) def test_line_total_within_tolerance(self): """line_total should match within tolerance.""" line = LineItem( position=1, description="Test Product", quantity=10, unit="Stück", unit_price=9.99, line_total=99.91, # Close enough to 99.90 (within 0.01) ) xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[line], totals=Totals(line_total_sum=99.91, net=99.91, vat_total=0.0, gross=99.91), ) errors = validate_betraege(xml_data) # No error should be raised for small differences within tolerance line_total_errors = [ e for e in errors if "line_total" in str(e.field) and e.error_code == "calculation_mismatch" ] assert len(line_total_errors) == 0 def test_totals_net_mismatch(self): """totals.net should equal sum of line_items.line_total.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product A", quantity=10, unit="Stück", unit_price=10.0, line_total=100.0, ), LineItem( position=2, description="Product B", quantity=5, unit="Stück", unit_price=10.0, line_total=50.0, ), ], totals=Totals( line_total_sum=150.0, net=160.0, # Wrong: 100 + 50 = 150 vat_total=0.0, gross=160.0, ), ) errors = validate_betraege(xml_data) assert any(e.field == "totals.net" for e in errors) assert any(e.error_code == "calculation_mismatch" for e in errors) def test_vat_breakdown_amount_mismatch(self): """vat_breakdown.amount should equal base × (rate/100).""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product", quantity=1, unit="Stück", unit_price=100.0, line_total=100.0, ) ], totals=Totals( line_total_sum=100.0, net=100.0, vat_total=20.0, gross=120.0, vat_breakdown=[ VatBreakdown( rate=19.0, base=100.0, amount=25.0 ) # Wrong: 100 × 0.19 = 19.0 ], ), ) errors = validate_betraege(xml_data) assert any("vat_breakdown" in e.field for e in errors) assert any(e.error_code == "calculation_mismatch" for e in errors) def test_totals_vat_total_mismatch(self): """totals.vat_total should equal sum of vat_breakdown.amount.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product", quantity=1, unit="Stück", unit_price=100.0, line_total=100.0, ) ], totals=Totals( line_total_sum=100.0, net=100.0, vat_total=25.0, # Wrong: 19.0 gross=125.0, vat_breakdown=[VatBreakdown(rate=19.0, base=100.0, amount=19.0)], ), ) errors = validate_betraege(xml_data) assert any(e.field == "totals.vat_total" for e in errors) assert any(e.error_code == "calculation_mismatch" for e in errors) def test_totals_gross_mismatch(self): """totals.gross should equal totals.net + totals.vat_total.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product", quantity=1, unit="Stück", unit_price=100.0, line_total=100.0, ) ], totals=Totals( line_total_sum=100.0, net=100.0, vat_total=19.0, gross=130.0, # Wrong: 100 + 19 = 119 vat_breakdown=[VatBreakdown(rate=19.0, base=100.0, amount=19.0)], ), ) errors = validate_betraege(xml_data) assert any(e.field == "totals.gross" for e in errors) assert any(e.error_code == "calculation_mismatch" for e in errors) def test_multiple_calculation_errors(self): """Multiple calculation errors should all be reported.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product", quantity=10, unit="Stück", unit_price=10.0, line_total=150.0, # Wrong: 10 × 10 = 100 ) ], totals=Totals( line_total_sum=150.0, net=200.0, # Wrong: should be 150 vat_total=30.0, # Wrong: 150 × 0.19 = 28.5 gross=250.0, # Wrong: 200 + 30 = 230 vat_breakdown=[ VatBreakdown( rate=19.0, base=150.0, amount=50.0 ) # Wrong: 150 × 0.19 = 28.5 ], ), ) errors = validate_betraege(xml_data) calc_errors = [e for e in errors if e.error_code == "calculation_mismatch"] assert len(calc_errors) > 1 class TestValidateUstid: """Tests for validate_ustid function.""" def test_valid_german_vat_id(self): """Valid German VAT ID should pass.""" error = validate_ustid("DE123456789") assert error is None def test_valid_austrian_vat_id(self): """Valid Austrian VAT ID should pass.""" error = validate_ustid("ATU12345678") assert error is None def test_valid_swiss_vat_id_mwst(self): """Valid Swiss VAT ID with MWST suffix should pass.""" error = validate_ustid("CHE123456789MWST") assert error is None def test_valid_swiss_vat_id_tva(self): """Valid Swiss VAT ID with TVA suffix should pass.""" error = validate_ustid("CHE123456789TVA") assert error is None def test_valid_swiss_vat_id_iva(self): """Valid Swiss VAT ID with IVA suffix should pass.""" error = validate_ustid("CHE123456789IVA") assert error is None def test_invalid_german_too_short(self): """German VAT ID too short should fail.""" error = validate_ustid("DE12345") assert error is not None assert error.error_code == "invalid_format" def test_invalid_german_too_long(self): """German VAT ID too long should fail.""" error = validate_ustid("DE1234567890") assert error is not None assert error.error_code == "invalid_format" def test_invalid_austrian_wrong_format(self): """Austrian VAT ID without U should fail.""" error = validate_ustid("AT12345678") assert error is not None assert error.error_code == "invalid_format" def test_invalid_swiss_wrong_suffix(self): """Swiss VAT ID with wrong suffix should fail.""" error = validate_ustid("CHE123456789XXX") assert error is not None assert error.error_code == "invalid_format" def test_invalid_country_code(self): """Unknown country code should fail.""" error = validate_ustid("FR123456789") assert error is not None assert error.error_code == "invalid_format" def test_empty_vat_id(self): """Empty VAT ID should fail.""" error = validate_ustid("") assert error is not None assert error.error_code == "invalid_format" class TestValidatePdfAbgleich: """Tests for validate_pdf_abgleich function.""" def test_exact_match_invoice_number(self): """Exact match for invoice_number should pass.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product", quantity=1, unit="Stück", unit_price=100.0, line_total=100.0, ) ], totals=Totals(line_total_sum=100.0, net=100.0, vat_total=19.0, gross=119.0), ) pdf_values = {"invoice_number": "INV001", "totals.gross": "119.00"} errors = validate_pdf_abgleich(xml_data, pdf_values) # No error for exact match invoice_errors = [e for e in errors if e.field == "invoice_number"] assert len(invoice_errors) == 0 def test_mismatch_invoice_number(self): """Mismatch in invoice_number should produce error.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product", quantity=1, unit="Stück", unit_price=100.0, line_total=100.0, ) ], totals=Totals(line_total_sum=100.0, net=100.0, vat_total=19.0, gross=119.0), ) pdf_values = {"invoice_number": "INV002"} errors = validate_pdf_abgleich(xml_data, pdf_values) assert any(e.field == "invoice_number" for e in errors) def test_match_totals_gross(self): """Matching totals.gross within tolerance should pass.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product", quantity=1, unit="Stück", unit_price=100.0, line_total=100.0, ) ], totals=Totals(line_total_sum=100.0, net=100.0, vat_total=19.0, gross=119.0), ) pdf_values = {"totals.gross": "119.00"} errors = validate_pdf_abgleich(xml_data, pdf_values) gross_errors = [e for e in errors if e.field == "totals.gross"] assert len(gross_errors) == 0 def test_mismatch_totals_gross(self): """Mismatch in totals.gross should produce error.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product", quantity=1, unit="Stück", unit_price=100.0, line_total=100.0, ) ], totals=Totals(line_total_sum=100.0, net=100.0, vat_total=19.0, gross=119.0), ) pdf_values = {"totals.gross": "150.00"} errors = validate_pdf_abgleich(xml_data, pdf_values) assert any(e.field == "totals.gross" for e in errors) def test_match_within_tolerance(self): """Values within tolerance should pass.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product", quantity=1, unit="Stück", unit_price=100.0, line_total=100.0, ) ], totals=Totals(line_total_sum=100.0, net=100.0, vat_total=19.0, gross=119.0), ) # 119.01 is within 0.01 of 119.0 pdf_values = {"totals.gross": "119.01"} errors = validate_pdf_abgleich(xml_data, pdf_values) gross_errors = [e for e in errors if e.field == "totals.gross"] assert len(gross_errors) == 0 def test_missing_pdf_value(self): """Missing PDF value should not produce error.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product", quantity=1, unit="Stück", unit_price=100.0, line_total=100.0, ) ], totals=Totals(line_total_sum=100.0, net=100.0, vat_total=19.0, gross=119.0), ) pdf_values = {} errors = validate_pdf_abgleich(xml_data, pdf_values) # No errors when PDF values are missing (can't compare) assert len(errors) == 0 def test_multiple_mismatches(self): """Multiple mismatches should all be reported.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product", quantity=1, unit="Stück", unit_price=100.0, line_total=100.0, ) ], totals=Totals(line_total_sum=100.0, net=100.0, vat_total=19.0, gross=119.0), ) pdf_values = { "invoice_number": "INV002", "totals.gross": "150.00", "totals.net": "120.00", "totals.vat_total": "25.00", } errors = validate_pdf_abgleich(xml_data, pdf_values) # Should have errors for all mismatches assert len(errors) >= 3 class TestValidateInvoice: """Tests for validate_invoice function.""" def test_run_single_check(self): """Run single validation check.""" xml_data = XmlData( invoice_number="", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product", quantity=1, unit="Stück", unit_price=100.0, line_total=100.0, ) ], totals=Totals(line_total_sum=100.0, net=100.0, vat_total=19.0, gross=119.0), ) request = ValidateRequest( xml_data=xml_data.model_dump(), checks=["pflichtfelder"], ) result = validate_invoice(request) assert result.is_valid is False # Missing invoice_number assert len(result.errors) > 0 assert result.summary is not None def test_run_multiple_checks(self): """Run multiple validation checks.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product", quantity=10, unit="Stück", unit_price=10.0, line_total=150.0, # Wrong: 10 × 10 = 100 ) ], totals=Totals( line_total_sum=150.0, net=200.0, vat_total=30.0, gross=250.0, vat_breakdown=[VatBreakdown(rate=19.0, base=150.0, amount=50.0)], ), ) request = ValidateRequest( xml_data=xml_data.model_dump(), checks=["pflichtfelder", "betraege", "ustid"], ) result = validate_invoice(request) assert result.is_valid is False assert result.summary is not None assert result.summary["total_checks"] == 3 # Should have errors from multiple checks total_issues = len(result.errors) + len(result.warnings) assert total_issues > 0 def test_all_checks_pass(self): """All checks passing should return is_valid=True.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", due_date="2024-02-15", supplier=Supplier( name="Test Supplier GmbH", vat_id="DE123456789", ), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product A", quantity=10.0, unit="Stück", unit_price=9.99, line_total=99.90, vat_rate=19.0, ), ], totals=Totals( line_total_sum=99.90, net=99.90, vat_total=18.98, gross=118.88, vat_breakdown=[VatBreakdown(rate=19.0, base=99.90, amount=18.98)], ), payment_terms=PaymentTerms(iban="DE89370400440532013000"), ) request = ValidateRequest( xml_data=xml_data.model_dump(), checks=["pflichtfelder", "betraege"], ) result = validate_invoice(request) assert result.is_valid is True assert len(result.errors) == 0 assert result.summary["checks_passed"] == 2 def test_warnings_not_critical(self): """Warnings should not make is_valid=False.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product", quantity=1, unit="Stück", unit_price=100.0, line_total=100.0, ) ], totals=Totals(line_total_sum=100.0, net=100.0, vat_total=19.0, gross=119.0), ) # Missing due_date and payment_terms.iban are warnings request = ValidateRequest( xml_data=xml_data.model_dump(), checks=["pflichtfelder"], ) result = validate_invoice(request) # Should have warnings but be valid assert len(result.warnings) > 0 assert result.is_valid is True # No critical errors def test_summary_counts_correctly(self): """Summary should correctly count checks and errors.""" xml_data = XmlData( invoice_number="", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product", quantity=1, unit="Stück", unit_price=100.0, line_total=150.0, ) ], totals=Totals( line_total_sum=150.0, net=200.0, vat_total=30.0, gross=250.0, vat_breakdown=[VatBreakdown(rate=19.0, base=150.0, amount=50.0)], ), ) request = ValidateRequest( xml_data=xml_data.model_dump(), checks=["pflichtfelder", "betraege", "ustid"], ) result = validate_invoice(request) assert result.summary is not None assert result.summary["total_checks"] == 3 assert "checks_passed" in result.summary assert "checks_failed" in result.summary assert "critical_errors" in result.summary assert "warnings" in result.summary def test_with_pdf_abgleich(self): """PDF comparison should work with validate_invoice.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product", quantity=1, unit="Stück", unit_price=100.0, line_total=100.0, ) ], totals=Totals(line_total_sum=100.0, net=100.0, vat_total=19.0, gross=119.0), ) request = ValidateRequest( xml_data=xml_data.model_dump(), pdf_text="Invoice INV001 Total: 150.00", # Mismatch checks=["pdf_abgleich"], ) result = validate_invoice(request) # Should have errors from PDF comparison assert result.summary["total_checks"] == 1 def test_validation_time_populated(self): """validation_time_ms should be populated.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product", quantity=1, unit="Stück", unit_price=100.0, line_total=100.0, ) ], totals=Totals(line_total_sum=100.0, net=100.0, vat_total=19.0, gross=119.0), ) request = ValidateRequest( xml_data=xml_data.model_dump(), checks=["pflichtfelder"], ) result = validate_invoice(request) assert result.validation_time_ms >= 0 def test_invalid_check_name_ignored(self): """Invalid check names should be ignored.""" xml_data = XmlData( invoice_number="INV001", invoice_date="2024-01-15", supplier=Supplier(name="Test Supplier GmbH", vat_id="DE123456789"), buyer=Buyer(name="Test Buyer AG"), line_items=[ LineItem( position=1, description="Product", quantity=1, unit="Stück", unit_price=100.0, line_total=100.0, ) ], totals=Totals(line_total_sum=100.0, net=100.0, vat_total=19.0, gross=119.0), ) request = ValidateRequest( xml_data=xml_data.model_dump(), checks=["invalid_check"], ) result = validate_invoice(request) # Should handle gracefully - no errors from invalid check assert result.summary["total_checks"] == 0 assert result.is_valid is True # No critical errors