Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/invoice2data/extract/template_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,22 @@


#: Capture pattern per candidate kind, used to build a field's value group.
#: These are TIGHT patterns applied at *extraction* time -- when the suggested
#: template is later run against a fresh PDF the label alone isn't enough to
#: anchor a value, so an overly loose pattern (``[A-Z0-9]+`` for BIC/VAT)
#: would grab any adjacent alphanumeric blob. The shapes here mirror the
#: strict validators in ``validators.py`` so a suggestion produces a regex
#: that will only re-match a properly shaped identifier.
_VALUE_PATTERNS = {
"date": r"\d[\d/.\-]+\d",
"amount": r"[\d.,]+",
"iban": r"[A-Z0-9 ]+",
"vat": r"[A-Z0-9]+",
"bic": r"[A-Z0-9]+",
# Contiguous or space-grouped IBAN body -- capture then strip in-app.
"iban": r"[A-Z]{2}\d{2}(?:[ \-]?[A-Z0-9]){11,30}",
# VAT: 2-letter country + 8-14 alphanumerics. Country slot MUST be alpha.
"vat": r"[A-Z]{2}[A-Z0-9]{8,14}",
# BIC / SWIFT (ISO 9362): 8 or 11 chars, first 6 letters, then 2 alnum,
# optionally 3 more (branch).
"bic": r"[A-Z]{6}[A-Z0-9]{2}(?:[A-Z0-9]{3})?",
}


Expand Down
25 changes: 25 additions & 0 deletions tests/test_template_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,31 @@ def test_field_regex_anchors_on_label() -> None:
assert match.group(1) == "121.00"


def test_bic_value_pattern_only_captures_bic_shape() -> None:
"""A suggested BIC regex must not slurp arbitrary alphanumeric blobs.

Regression: the value pattern used to be ``[A-Z0-9]+`` which, when the
suggestion was later applied to a fresh PDF, gobbled adjacent uppercase
tokens (order codes, postal codes, single letters). Now the ISO-9362
shape is enforced at extraction time.
"""
text = "ACME Corp\nInvoice #ORDER12345\nBank: DEUTDEFFXXX\n"
template = suggested_template(text)
bic_pattern = template["fields"].get("bic")
assert bic_pattern, "expected a bic suggestion"
matches = re.findall(bic_pattern, text)
assert matches == ["DEUTDEFFXXX"], (
f"tight pattern should only capture the actual BIC; got {matches!r}"
)
# Explicitly: an 8-char order code alone should NOT match the bic pattern
# even without any surrounding label anchor.
from invoice2data.extract.template_builder import _VALUE_PATTERNS

assert re.fullmatch(_VALUE_PATTERNS["bic"], "ORDER123") is None
assert re.fullmatch(_VALUE_PATTERNS["bic"], "DEUTDEFF") is not None
assert re.fullmatch(_VALUE_PATTERNS["bic"], "DEUTDEFFXXX") is not None


def test_to_yaml_roundtrips() -> None:
template = {
"issuer": "ACME",
Expand Down
Loading