b9baa8c603
Erkennt B2C-Shop ohne öffentlich erreichbare Widerrufsbelehrung.
Schließt eine der offenen GT-Lücken aus dem Elli-Audit.
Signale:
- doc_entries[widerruf]: discovery_attempted=True + Text leer
- kein Footer-Link auf Widerruf/cancellation/rückgabe
- B2C-Scope: Warenkorb/Kasse/Bestellung/MwSt/Wallbox/Tarif (strong)
vs Shop/Produkt/Rechnung (weak, ≥2 = likely)
- B2B-only-Override: "ausschließlich an Unternehmer" etc.
Severity:
- HIGH bei b2c_strong
- MEDIUM bei b2c_likely
- kein Finding bei b2b_only / unknown (False-Positive-Schutz)
Norm: Art. 246a § 1 Abs. 2 Nr. 1 EGBGB i.V.m. § 312d BGB.
Wiring:
- widerrufsbelehrung_reachability_check.py — Check + Scope-Detection
- _b13_wiring.py — Render + state-Anschluss
- _orchestrator.py — run_b13 nach run_b12
- mail_render_v2/_compose.py — widerruf_reach_html-Block
Tests: 13/13 grün (Scope-Detection 5 + Check-Logik 8).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
68 lines
2.5 KiB
Python
68 lines
2.5 KiB
Python
"""B13 wiring — Widerrufsbelehrung-Reachability.
|
|
|
|
Hängt sich an `state["extra_findings"]` an und rendert einen
|
|
eigenständigen V2-HTML-Block (`widerruf_reach_html`).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import html
|
|
import logging
|
|
|
|
from compliance.services.widerrufsbelehrung_reachability_check import (
|
|
check_widerrufsbelehrung_reachability,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def run_b13(state: dict) -> None:
|
|
new = check_widerrufsbelehrung_reachability(state)
|
|
if not new:
|
|
return
|
|
extras = state.get("extra_findings") or []
|
|
extras.extend(new)
|
|
state["extra_findings"] = extras
|
|
state["widerruf_reach_html"] = _render(new)
|
|
logger.info("B13 widerruf-reach: %d finding(s)", len(new))
|
|
|
|
|
|
def _render(findings: list[dict]) -> str:
|
|
cards = []
|
|
for f in findings:
|
|
sev = (f.get("severity") or "").upper()
|
|
color = "#dc2626" if sev == "HIGH" else "#f59e0b"
|
|
scope_tag = f.get("b2c_scope") or ""
|
|
scope_html = (
|
|
f"<span style='display:inline-block;background:#fef3c7;"
|
|
f"color:#92400e;font-size:10px;padding:1px 6px;border-radius:999px;"
|
|
f"margin-left:6px;'>Scope: {html.escape(scope_tag)}</span>"
|
|
if scope_tag else ""
|
|
)
|
|
cards.append(
|
|
f"<div style='margin:12px 0;padding:14px;background:#fff;"
|
|
f"border-left:3px solid {color};border-radius:4px;'>"
|
|
f"<div style='font-weight:600;color:{color};font-size:14px;'>"
|
|
f"{sev} · {html.escape(f.get('check_id') or '')}{scope_html}</div>"
|
|
f"<div style='font-size:14px;margin-top:4px;'>"
|
|
f"<strong>{html.escape(f.get('title') or '')}</strong></div>"
|
|
f"<div style='font-size:12px;color:#64748b;margin-top:2px;'>"
|
|
f"{html.escape(f.get('norm') or '')}</div>"
|
|
f"<div style='font-size:12px;color:#475569;margin-top:6px;'>"
|
|
f"<em>{html.escape(f.get('evidence') or '')}</em></div>"
|
|
f"<div style='font-size:13px;margin-top:8px;background:#dcfce7;"
|
|
f"padding:8px 10px;border-radius:4px;'>"
|
|
f"<strong>→ Empfehlung:</strong> "
|
|
f"{html.escape(f.get('action') or '')}</div>"
|
|
"</div>"
|
|
)
|
|
return (
|
|
"<div style='margin:24px 0;padding:16px;border-left:4px solid #dc2626;"
|
|
"background:#fef2f2;border-radius:4px;'>"
|
|
"<h2 style='margin:0 0 8px;color:#7f1d1d;font-size:16px;'>"
|
|
"📜 Widerrufsbelehrung-Reachability (B2C-Pflicht)"
|
|
"</h2>"
|
|
+ "".join(cards) +
|
|
"</div>"
|
|
)
|