Files
breakpilot-compliance/backend-compliance/compliance/services/impressum_multi_entity_check.py
T
Benjamin Admin 8b9cad88ae fix(b9): clean entity names in multi-entity-impressum (GT IMPRESSUM-001)
Der Multi-Entity-Check fängt Elli's USt-IdNr-Lücke (VW Group Charging
GmbH hat keine, Elli Mobility GmbH hat eine), aber Entity-Namen waren
mit Header-Noise verunreinigt:

  'Impressum\n\nVolkswagen Group Charging GmbH'
  'eco\n\nElli Mobility GmbH'

Behoben:
  - _ENTITY_PAT lässt nur Space im Namen zu (kein \s/\n mehr)
  - _clean_entity_name() trimmt Header-Worte (Impressum, Anbieter, ...)
    und nimmt nur die letzte Zeile vor Legal-Form-Suffix
  - 11 neue Tests, davon einer mit Elli-like Impressum als
    Charakterisierungs-Test

Damit ist die finale Finding-Ausgabe für Audit-Reports lesbar
('Fehlt bei: Volkswagen Group Charging GmbH') statt verunreinigt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 00:08:18 +02:00

117 lines
4.3 KiB
Python

"""B9 — Multi-Entity-Impressum-Check.
Findings, wenn ein Impressum mehrere Entitäten (mehrere GmbH/AG/UG)
nennt, aber Pflichtangaben nur bei einer davon vollständig sind.
Konkreter Elli-Pattern (GT IMPRESSUM-001):
- Entity 1: "Elli Mobility GmbH ... USt-IdNr DE814424009 ..."
- Entity 2: "VW Group Charging GmbH ... [keine USt-IdNr] ..."
→ USt-IdNr fehlt bei Entity 2.
Heuristik:
1. Entitäten erkennen: jede Match auf "<Name> (GmbH|AG|UG|KG|SE)" als
Entity-Boundary; Text-Slice von dort bis zur nächsten Entity.
2. Pro Entity prüfen: USt-IdNr, Handelsregister, Vertretungsberechtigte.
3. Wenn Entity N ein Feld nennt, das Entity M nicht hat → MEDIUM.
"""
from __future__ import annotations
import logging
import re
logger = logging.getLogger(__name__)
_ENTITY_PAT = re.compile(
r"([A-ZÄÖÜ][\w\-\& ]{1,50}?\s+(?:GmbH|AG|UG|KG|SE|"
r"e\.V\.|GbR|OHG|Limited|Ltd|LLC))",
re.IGNORECASE,
)
_NAME_NOISE_PAT = re.compile(
r"^(?:Impressum|Anbieter|Anbieterkennzeichnung|Diensteanbieter|"
r"Verantwortlich(?:er)?|Kontakt|Adresse|@\S+|.+@.+)\s*[:|\-]?\s*",
re.IGNORECASE,
)
_USTID_PAT = re.compile(r"\b(?:USt-?Id(?:Nr)?\.?|VAT(?:-?Id)?)\s*[:.\s]\s*"
r"(DE\d{8,10}|[A-Z]{2}\d{6,12})", re.IGNORECASE)
_HR_PAT = re.compile(r"\b(?:HR[BA]|Handelsregister|Registergericht)"
r"\s*[:.\s]*([\w\s\d\-/]{4,80})", re.IGNORECASE)
_GF_PAT = re.compile(r"(?:Geschäftsführer|Vertretungsberechtigt|"
r"vertreten\s+durch)\s*[:.\s]+", re.IGNORECASE)
def _clean_entity_name(raw: str) -> str:
"""Strip leading header noise + collapse whitespace."""
name = raw.strip()
# If the match spans multiple lines (regex captured a header before
# the actual company name), keep only the last line.
if "\n" in name:
name = name.rsplit("\n", 1)[-1].strip()
name = _NAME_NOISE_PAT.sub("", name).strip()
return re.sub(r"\s+", " ", name)
def _slice_entities(text: str) -> list[tuple[str, str]]:
"""Return [(entity_name, text_slice)] for each detected entity."""
matches = list(_ENTITY_PAT.finditer(text))
if len(matches) < 2:
return []
slices: list[tuple[str, str]] = []
for i, m in enumerate(matches):
start = m.start()
end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
slices.append((_clean_entity_name(m.group(1)), text[start:end]))
return slices
def check_multi_entity_impressum(state: dict) -> list[dict]:
doc_texts = state.get("doc_texts") or {}
imp = doc_texts.get("impressum") or ""
if not imp:
return []
slices = _slice_entities(imp)
if not slices:
return []
# Compute features per entity
features = []
for name, slc in slices:
features.append({
"name": name,
"ust_id": bool(_USTID_PAT.search(slc)),
"hr": bool(_HR_PAT.search(slc)),
"gf": bool(_GF_PAT.search(slc)),
})
# If ALL share the same flags → no inconsistency
findings: list[dict] = []
for field, label in (
("ust_id", "USt-IdNr."),
("hr", "Handelsregister-Eintrag"),
("gf", "Vertretungsberechtigte"),
):
present = [f for f in features if f[field]]
missing = [f for f in features if not f[field]]
if present and missing and len(present) >= 1:
findings.append({
"check_id": f"IMPRESSUM-MULTI-{field.upper()}",
"severity": "MEDIUM",
"severity_reason": "incomplete",
"title": (
f"{label} fehlt bei "
f"{len(missing)} von {len(features)} Entitäten"
),
"norm": "§ 5 Abs. 1 TMG (Pflichtangabe pro Diensteanbieter)",
"entities_present": [f["name"] for f in present],
"entities_missing": [f["name"] for f in missing],
"action": (
f"{label} im Impressum für "
f"{', '.join(f['name'] for f in missing)} ergänzen. "
"Pflichtangabe ist pro Diensteanbieter zu erfüllen, "
"nicht 'eine reicht für alle'."
),
})
if findings:
logger.info("B9 multi-entity impressum: %d findings", len(findings))
return findings