Allow zero-euro line item amounts
This commit is contained in:
+12
-12
@@ -4,8 +4,10 @@ Tests are written following TDD: FAILING TESTS FIRST (RED phase),
|
||||
then implementation makes them pass (GREEN phase).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import base64
|
||||
import binascii
|
||||
|
||||
import pytest
|
||||
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
@@ -49,7 +51,7 @@ class TestFileSizeValidation:
|
||||
|
||||
def test_file_size_limit_exactly_10mb(self):
|
||||
"""Test PDF exactly at 10MB limit passes size check but fails PDF parsing."""
|
||||
from src.extractor import extract_zugferd, ExtractionError
|
||||
from src.extractor import ExtractionError, extract_zugferd
|
||||
|
||||
# 10MB = 10 * 1024 * 1024 bytes
|
||||
large_pdf = b"X" * (10 * 1024 * 1024)
|
||||
@@ -63,7 +65,7 @@ class TestFileSizeValidation:
|
||||
|
||||
def test_file_size_limit_10mb_plus_one_byte(self):
|
||||
"""Test PDF one byte over 10MB limit is rejected."""
|
||||
from src.extractor import extract_zugferd, ExtractionError
|
||||
from src.extractor import ExtractionError, extract_zugferd
|
||||
|
||||
# 10MB + 1 byte
|
||||
too_large = b"X" * (10 * 1024 * 1024 + 1)
|
||||
@@ -75,7 +77,7 @@ class TestFileSizeValidation:
|
||||
|
||||
def test_file_size_under_10mb_accepted(self):
|
||||
"""Test PDF under 10MB is accepted for processing."""
|
||||
from src.extractor import extract_zugferd, ExtractionError
|
||||
from src.extractor import ExtractionError, extract_zugferd
|
||||
|
||||
# Small PDF (9MB)
|
||||
small_pdf = b"X" * (9 * 1024 * 1024)
|
||||
@@ -166,8 +168,8 @@ class TestEN16931Extraction:
|
||||
assert first_item.description is not None and len(first_item.description) > 0
|
||||
assert first_item.quantity > 0
|
||||
assert first_item.unit is not None and len(first_item.unit) > 0
|
||||
assert first_item.unit_price > 0
|
||||
assert first_item.line_total > 0
|
||||
assert first_item.unit_price is not None and first_item.unit_price > 0
|
||||
assert first_item.line_total is not None and first_item.line_total > 0
|
||||
|
||||
# Totals
|
||||
assert xml_data.totals.line_total_sum > 0
|
||||
@@ -181,7 +183,7 @@ class TestErrorHandling:
|
||||
|
||||
def test_corrupt_pdf_raises_error(self):
|
||||
"""Test corrupt PDF raises ExtractionError with correct code."""
|
||||
from src.extractor import extract_zugferd, ExtractionError
|
||||
from src.extractor import ExtractionError, extract_zugferd
|
||||
|
||||
# Invalid PDF data
|
||||
corrupt_pdf = b"NOT A PDF FILE AT ALL"
|
||||
@@ -194,14 +196,14 @@ class TestErrorHandling:
|
||||
|
||||
def test_empty_pdf_raises_error(self):
|
||||
"""Test empty PDF raises ExtractionError."""
|
||||
from src.extractor import extract_zugferd, ExtractionError
|
||||
from src.extractor import ExtractionError, extract_zugferd
|
||||
|
||||
with pytest.raises(ExtractionError):
|
||||
extract_zugferd(b"")
|
||||
|
||||
def test_invalid_base64(self):
|
||||
"""Test invalid base64 raises ExtractionError."""
|
||||
from src.extractor import extract_zugferd, ExtractionError
|
||||
from src.extractor import ExtractionError, extract_zugferd
|
||||
|
||||
# This would be called by API layer, but we can test the concept
|
||||
# Invalid PDF that's not valid base64-encoded
|
||||
@@ -210,7 +212,7 @@ class TestErrorHandling:
|
||||
# If API layer decodes invalid base64, it gets error
|
||||
decoded = base64.b64decode(invalid_base64, validate=True)
|
||||
extract_zugferd(decoded)
|
||||
except (base64.binascii.Error, ValueError):
|
||||
except (binascii.Error, ValueError):
|
||||
# base64 error is expected
|
||||
pass
|
||||
except ExtractionError as e:
|
||||
@@ -232,8 +234,6 @@ class TestPDFTextExtraction:
|
||||
|
||||
assert result.pdf_text is not None
|
||||
assert len(result.pdf_text) > 0
|
||||
# Should contain some common German invoice terms
|
||||
text_lower = result.pdf_text.lower()
|
||||
# PDF text may contain invoice-related terms in German or English
|
||||
|
||||
|
||||
|
||||
+32
-9
@@ -1,6 +1,5 @@
|
||||
"""Tests for ZUGFeRD validator using TDD approach."""
|
||||
|
||||
import pytest
|
||||
from src.models import (
|
||||
XmlData,
|
||||
Supplier,
|
||||
@@ -9,8 +8,6 @@ from src.models import (
|
||||
Totals,
|
||||
VatBreakdown,
|
||||
PaymentTerms,
|
||||
ErrorDetail,
|
||||
ValidationResult,
|
||||
ValidateRequest,
|
||||
)
|
||||
from src.validator import (
|
||||
@@ -352,8 +349,8 @@ class TestValidatePflichtfelder:
|
||||
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."""
|
||||
def test_zero_euro_line_item_amounts_allowed(self):
|
||||
"""Zero-valued line item prices should count as present."""
|
||||
xml_data = XmlData(
|
||||
invoice_number="INV001",
|
||||
invoice_date="2024-01-15",
|
||||
@@ -366,6 +363,29 @@ class TestValidatePflichtfelder:
|
||||
quantity=1.0,
|
||||
unit="Stück",
|
||||
unit_price=0.0,
|
||||
line_total=0.0,
|
||||
)
|
||||
],
|
||||
totals=Totals(line_total_sum=10.0, net=10.0, vat_total=19.0, gross=29.0),
|
||||
)
|
||||
errors = validate_pflichtfelder(xml_data)
|
||||
assert not any(e.field == "line_items[0].unit_price" for e in errors)
|
||||
assert not any(e.field == "line_items[0].line_total" for e in errors)
|
||||
|
||||
def test_missing_line_item_unit_price_critical(self):
|
||||
"""Missing line_item.unit_price 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=None,
|
||||
line_total=10.0,
|
||||
)
|
||||
],
|
||||
@@ -376,7 +396,7 @@ class TestValidatePflichtfelder:
|
||||
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."""
|
||||
"""Missing line_item.line_total should produce critical error."""
|
||||
xml_data = XmlData(
|
||||
invoice_number="INV001",
|
||||
invoice_date="2024-01-15",
|
||||
@@ -389,7 +409,7 @@ class TestValidatePflichtfelder:
|
||||
quantity=1.0,
|
||||
unit="Stück",
|
||||
unit_price=10.0,
|
||||
line_total=0.0,
|
||||
line_total=None,
|
||||
)
|
||||
],
|
||||
totals=Totals(line_total_sum=10.0, net=10.0, vat_total=0.0, gross=10.0),
|
||||
@@ -499,7 +519,7 @@ class TestValidateBetraege:
|
||||
errors = validate_betraege(xml_data)
|
||||
assert any(e.error_code == "calculation_mismatch" for e in errors)
|
||||
assert any(
|
||||
"line_total" in e.field
|
||||
e.field is not None and "line_total" in e.field
|
||||
for e in errors
|
||||
if e.error_code == "calculation_mismatch"
|
||||
)
|
||||
@@ -597,7 +617,7 @@ class TestValidateBetraege:
|
||||
),
|
||||
)
|
||||
errors = validate_betraege(xml_data)
|
||||
assert any("vat_breakdown" in e.field for e in errors)
|
||||
assert any(e.field is not None and "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):
|
||||
@@ -1040,6 +1060,7 @@ class TestValidateInvoice:
|
||||
result = validate_invoice(request)
|
||||
assert result.is_valid is True
|
||||
assert len(result.errors) == 0
|
||||
assert result.summary is not None
|
||||
assert result.summary["checks_passed"] == 2
|
||||
|
||||
def test_warnings_not_critical(self):
|
||||
@@ -1134,6 +1155,7 @@ class TestValidateInvoice:
|
||||
)
|
||||
result = validate_invoice(request)
|
||||
# Should have errors from PDF comparison
|
||||
assert result.summary is not None
|
||||
assert result.summary["total_checks"] == 1
|
||||
|
||||
def test_validation_time_populated(self):
|
||||
@@ -1187,5 +1209,6 @@ class TestValidateInvoice:
|
||||
)
|
||||
result = validate_invoice(request)
|
||||
# Should handle gracefully - no errors from invalid check
|
||||
assert result.summary is not None
|
||||
assert result.summary["total_checks"] == 0
|
||||
assert result.is_valid is True # No critical errors
|
||||
|
||||
Reference in New Issue
Block a user