From 3ffa3f57931fc392550a9c8970e8dd4cb84b4541 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Fri, 24 Apr 2026 08:39:55 +0200 Subject: [PATCH] =?UTF-8?q?feat(control-pipeline):=20add=20Document=20Comp?= =?UTF-8?q?liance=20Engine=20=E2=80=94=20scope=20detection=20+=20document?= =?UTF-8?q?=20requirements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New service: document_scope_resolver.py with 28 document rules covering: - Base (impressum, privacy_policy) - Tracking (cookie_banner, cookie_policy) - E-Commerce (AGB, withdrawal, shipping, pricing, payment) - Digital (digital_content_terms, no_withdrawal_notice) - SaaS (ToS, service_description, DPA, SLA) - AI (transparency_notice, automated_decisions) - Hardware (warranty, return, CE, safety) - Environmental (WEEE, battery disposal) - Marketplace (seller terms, ranking transparency) - Subscription (cancellation terms) API: POST /v1/document-compliance/required Input: company flags + jurisdiction → Output: required documents + assessment Includes confidence scoring, escalation detection (e.g. ecommerce without distance_selling flag), and reasoning. 19 tests covering all business model combinations including B2B-only exclusions. Co-Authored-By: Claude Opus 4.6 (1M context) --- control-pipeline/api/__init__.py | 2 + .../api/document_compliance_routes.py | 36 ++ .../services/document_scope_resolver.py | 483 ++++++++++++++++++ .../tests/test_document_compliance.py | 190 +++++++ 4 files changed, 711 insertions(+) create mode 100644 control-pipeline/api/document_compliance_routes.py create mode 100644 control-pipeline/services/document_scope_resolver.py create mode 100644 control-pipeline/tests/test_document_compliance.py diff --git a/control-pipeline/api/__init__.py b/control-pipeline/api/__init__.py index decc0ad..4a41927 100644 --- a/control-pipeline/api/__init__.py +++ b/control-pipeline/api/__init__.py @@ -2,7 +2,9 @@ from fastapi import APIRouter from api.control_generator_routes import router as generator_router from api.canonical_control_routes import router as canonical_router +from api.document_compliance_routes import router as document_router router = APIRouter() router.include_router(generator_router) router.include_router(canonical_router) +router.include_router(document_router) diff --git a/control-pipeline/api/document_compliance_routes.py b/control-pipeline/api/document_compliance_routes.py new file mode 100644 index 0000000..7d91b7e --- /dev/null +++ b/control-pipeline/api/document_compliance_routes.py @@ -0,0 +1,36 @@ +""" +FastAPI routes for Document Compliance — required documents per business model. + +Endpoints: + POST /v1/document-compliance/required — Determine required documents +""" + +from typing import Optional + +from fastapi import APIRouter +from pydantic import BaseModel + +from services.document_scope_resolver import resolve_required_documents + +router = APIRouter(prefix="/v1/document-compliance", tags=["document-compliance"]) + + +class DocumentComplianceRequest(BaseModel): + flags: dict[str, bool] + jurisdiction: str = "DE" + + +@router.post("/required") +async def get_required_documents(req: DocumentComplianceRequest): + """Determine which legal documents are required based on company flags. + + Flags example: + has_website, has_ecommerce, has_saas, sells_physical_products, + distance_selling, uses_tracking, uses_ai, b2b_only, etc. + + Returns required + recommended documents with legal basis and assessment. + """ + return resolve_required_documents( + flags=req.flags, + jurisdiction=req.jurisdiction, + ) diff --git a/control-pipeline/services/document_scope_resolver.py b/control-pipeline/services/document_scope_resolver.py new file mode 100644 index 0000000..c7e08cb --- /dev/null +++ b/control-pipeline/services/document_scope_resolver.py @@ -0,0 +1,483 @@ +""" +Document Scope Resolver — determines which legal documents are required +based on company flags (website, ecommerce, SaaS, hardware, AI, etc.). + +Deterministic, no LLM needed. Complements the Applicability Engine +(which handles Controls) with a document-level compliance layer. + +Usage: + result = resolve_required_documents( + flags={"has_website": True, "has_ecommerce": True, "distance_selling": True}, + jurisdiction="DE", + ) + # result["required_documents"] → list of required document types + # result["assessment"] → confidence, escalation, reasoning +""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass, field, asdict +from typing import Any, Optional + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Document Requirement Rules +# --------------------------------------------------------------------------- + +DOCUMENT_RULES: dict[str, dict[str, Any]] = { + # === IMMER bei Website === + "impressum": { + "label": "Impressum / Anbieterkennzeichnung", + "required_if_any": ["has_website"], + "jurisdiction": ["DE", "AT"], + "legal_basis": "TMG § 5 / MedienG § 24", + "mandatory": True, + "category": "base", + }, + "privacy_policy": { + "label": "Datenschutzerklaerung", + "required_if_any": ["has_website", "has_user_accounts"], + "jurisdiction": ["DE", "AT", "EU"], + "legal_basis": "DSGVO Art. 13/14", + "mandatory": True, + "category": "base", + }, + + # === Bei Tracking/Cookies === + "cookie_banner": { + "label": "Cookie-Banner mit Einwilligungsmanagement", + "required_if_any": ["uses_tracking", "uses_cookies_marketing"], + "not_required_if": ["strictly_necessary_cookies_only"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "TTDSG § 25, ePrivacy-RL 2002/58/EG", + "mandatory": True, + "category": "tracking", + }, + "cookie_policy": { + "label": "Cookie-Richtlinie / Cookie-Details", + "required_if_any": ["uses_tracking", "uses_cookies_marketing"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "TTDSG § 25", + "mandatory": True, + "category": "tracking", + "note": "Kann Teil der Datenschutzerklaerung sein", + }, + + # === Bei E-Commerce === + "terms_and_conditions": { + "label": "Allgemeine Geschaeftsbedingungen (AGB)", + "required_if_any": ["has_ecommerce", "has_saas"], + "jurisdiction": ["DE", "AT"], + "legal_basis": "BGB §§ 305ff", + "mandatory": True, + "category": "ecommerce", + }, + "agb_checkout_summary": { + "label": "Vertragswesentliche Informationen im Checkout", + "required_if_any": ["has_checkout"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "Consumer Rights Directive Art. 8(2), BGB § 312j", + "mandatory": True, + "category": "ecommerce", + "note": "Preis, Laufzeit, Kuendigungsbedingungen VOR Bestellung sichtbar", + }, + "withdrawal_policy": { + "label": "Widerrufsbelehrung", + "required_if_any": ["distance_selling"], + "not_required_if": ["b2b_only"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "Consumer Rights Directive Art. 9-16, BGB § 355ff", + "mandatory": True, + "category": "ecommerce", + }, + "pricing_transparency": { + "label": "Preisangaben / Preistransparenz", + "required_if_any": ["has_ecommerce"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "PAngV, Consumer Rights Directive Art. 6(1)(e)", + "mandatory": True, + "category": "ecommerce", + }, + "shipping_information": { + "label": "Versand- und Lieferinformationen", + "required_if_any": ["sells_physical_products"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "Consumer Rights Directive Art. 6(1)(g)", + "mandatory": True, + "category": "ecommerce", + }, + "payment_terms": { + "label": "Zahlungsbedingungen und akzeptierte Zahlungsmittel", + "required_if_any": ["has_ecommerce"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "Consumer Rights Directive Art. 6(1)(g), BGB § 312d", + "mandatory": True, + "category": "ecommerce", + }, + + # === Digitaler Verkauf === + "digital_content_terms": { + "label": "Vertragsbedingungen fuer digitale Inhalte", + "required_if_any": ["sells_digital_products"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "Consumer Rights Directive Art. 5(2), BGB § 327ff", + "mandatory": True, + "category": "digital", + }, + "no_withdrawal_notice": { + "label": "Hinweis auf Widerrufsverzicht bei sofortiger Ausfuehrung", + "required_if_any": ["sells_digital_products"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "Consumer Rights Directive Art. 16(m), BGB § 356(5)", + "mandatory": True, + "category": "digital", + "note": "Checkbox: Zustimmung zur sofortigen Ausfuehrung + Kenntnis Widerrufsverlust", + }, + + # === SaaS === + "terms_of_service": { + "label": "Nutzungsbedingungen / Terms of Service", + "required_if_any": ["has_saas"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "BGB, DSGVO", + "mandatory": True, + "category": "saas", + }, + "service_description": { + "label": "Leistungsbeschreibung", + "required_if_any": ["has_saas"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "Consumer Rights Directive Art. 6(1)(a), BGB § 312d", + "mandatory": True, + "category": "saas", + }, + "data_processing_agreement": { + "label": "Auftragsverarbeitungsvertrag (AVV/DPA)", + "required_if_any": ["has_saas"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "DSGVO Art. 28", + "mandatory": True, + "category": "saas", + "note": "Pflicht wenn personenbezogene Daten im Auftrag verarbeitet werden", + }, + "sla": { + "label": "Service Level Agreement (SLA)", + "required_if_any": ["has_saas"], + "not_required_if": ["b2c_only"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "Vertragsrecht", + "mandatory": False, + "category": "saas", + "note": "Empfohlen fuer B2B SaaS", + }, + "acceptable_use_policy": { + "label": "Acceptable Use Policy / Nutzungsrichtlinie", + "required_if_any": ["has_saas", "operates_marketplace"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "Vertragsrecht, DSA (EU) 2022/2065", + "mandatory": False, + "category": "saas", + }, + + # === KI === + "ai_transparency_notice": { + "label": "KI-Transparenzhinweis", + "required_if_any": ["uses_ai"], + "jurisdiction": ["EU"], + "legal_basis": "AI Act Art. 52", + "mandatory": True, + "category": "ai", + }, + "automated_decision_explanation": { + "label": "Erklaerung automatisierter Entscheidungen", + "required_if_any": ["automated_decisions"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "DSGVO Art. 22", + "mandatory": True, + "category": "ai", + }, + + # === Hardware === + "warranty_information": { + "label": "Gewaehrleistungs- und Garantieinformationen", + "required_if_any": ["sells_physical_products"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "BGB § 437ff, Warenkauf-RL (EU) 2019/771", + "mandatory": True, + "category": "hardware", + }, + "return_policy": { + "label": "Rueckgabe- und Ruecksendebedingungen", + "required_if_any": ["distance_selling"], + "not_required_if": ["b2b_only"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "Consumer Rights Directive Art. 9-16", + "mandatory": True, + "category": "hardware", + }, + "ce_conformity_declaration": { + "label": "EU-Konformitaetserklaerung (CE)", + "required_if_any": ["sells_regulated_products"], + "jurisdiction": ["EU"], + "legal_basis": "Maschinenverordnung (EU) 2023/1230, Blue Guide", + "mandatory": True, + "category": "hardware", + }, + "product_safety_instructions": { + "label": "Sicherheitshinweise und Bedienungsanleitung", + "required_if_any": ["sells_regulated_products"], + "jurisdiction": ["EU"], + "legal_basis": "Produktsicherheitsverordnung (EU) 2023/988", + "mandatory": True, + "category": "hardware", + }, + + # === Umwelt / Batterie === + "weee_information": { + "label": "WEEE-Registrierung und Entsorgungshinweise", + "required_if_any": ["sells_electronics"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "WEEE-RL 2012/19/EU, ElektroG", + "mandatory": True, + "category": "environmental", + }, + "battery_disposal_information": { + "label": "Batterie-Entsorgungshinweise", + "required_if_any": ["contains_battery"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "Batterieverordnung (EU) 2023/1542", + "mandatory": True, + "category": "environmental", + }, + + # === Marketplace === + "marketplace_seller_terms": { + "label": "Haendler-AGB / Plattform-Teilnahmebedingungen", + "required_if_any": ["operates_marketplace"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "P2B-VO (EU) 2019/1150", + "mandatory": True, + "category": "marketplace", + }, + "marketplace_ranking_transparency": { + "label": "Transparenz zu Ranking- und Listungskriterien", + "required_if_any": ["operates_marketplace"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "P2B-VO (EU) 2019/1150 Art. 5", + "mandatory": True, + "category": "marketplace", + }, + + # === Subscription === + "subscription_cancellation_terms": { + "label": "Kuendigungsbedingungen bei Abonnements", + "required_if_any": ["subscription_model"], + "jurisdiction": ["DE", "EU"], + "legal_basis": "BGB § 309 Nr. 9, Faire-Verbrauchervertraege-RL", + "mandatory": True, + "category": "subscription", + }, +} + +# Signals that indicate the company is B2C (not B2B-only) +_B2C_INDICATORS = {"distance_selling", "has_checkout", "sells_digital_products"} + + +# --------------------------------------------------------------------------- +# Resolver +# --------------------------------------------------------------------------- + + +@dataclass +class DocumentRequirement: + """A single required document.""" + document_type: str + label: str + mandatory: bool + legal_basis: str + category: str + reason: str + note: Optional[str] = None + + +@dataclass +class DocumentAssessment: + """Assessment of document requirements completeness.""" + confidence: float = 1.0 + escalation_flag: bool = False + escalation_reason: Optional[str] = None + reasoning: str = "" + warnings: list = field(default_factory=list) + + +def resolve_required_documents( + flags: dict[str, bool], + jurisdiction: str = "DE", +) -> dict[str, Any]: + """Determine which legal documents are required based on company flags. + + Args: + flags: dict of scope signals (e.g., has_website, has_ecommerce, uses_ai) + jurisdiction: country code (DE, AT, EU) + + Returns: + dict with required_documents, total_required, total_recommended, assessment + """ + active_signals = {k for k, v in flags.items() if v is True} + required = [] + recommended = [] + + for doc_type, rule in DOCUMENT_RULES.items(): + # Check jurisdiction — DE and AT are part of EU + rule_jurisdictions = rule.get("jurisdiction", ["DE", "EU"]) + jurisdiction_match = ( + jurisdiction in rule_jurisdictions + or (jurisdiction in ("DE", "AT") and "EU" in rule_jurisdictions) + ) + if not jurisdiction_match: + continue + + # Check if any required signal is active + triggers = rule.get("required_if_any", []) + if not any(sig in active_signals for sig in triggers): + continue + + # Check exclusions + exclusions = rule.get("not_required_if", []) + if any(sig in active_signals for sig in exclusions): + continue + + # Determine triggered signals for reasoning + matched_signals = [sig for sig in triggers if sig in active_signals] + reason = f"Aktiv wegen: {', '.join(matched_signals)}" + + doc = DocumentRequirement( + document_type=doc_type, + label=rule.get("label", doc_type), + mandatory=rule.get("mandatory", True), + legal_basis=rule.get("legal_basis", ""), + category=rule.get("category", "other"), + reason=reason, + note=rule.get("note"), + ) + + if doc.mandatory: + required.append(doc) + else: + recommended.append(doc) + + # Assessment + assessment = _assess_documents(flags, active_signals, required, recommended) + + return { + "required_documents": [asdict(d) for d in required], + "recommended_documents": [asdict(d) for d in recommended], + "total_required": len(required), + "total_recommended": len(recommended), + "jurisdiction": jurisdiction, + "active_flags": sorted(active_signals), + "assessment": asdict(assessment), + } + + +def _assess_documents( + flags: dict[str, bool], + active_signals: set[str], + required: list[DocumentRequirement], + recommended: list[DocumentRequirement], +) -> DocumentAssessment: + """Compute assessment for document requirements.""" + assessment = DocumentAssessment() + warnings = [] + + # Confidence scoring + score = 0.0 + + # Has website flag? (+0.20) + if "has_website" in active_signals: + score += 0.20 + else: + warnings.append("has_website nicht gesetzt — Basis-Dokumente koennten fehlen") + + # E-commerce flags specified? (+0.20) + ecom_flags = {"has_ecommerce", "has_checkout", "distance_selling", "b2b_only"} + if active_signals & ecom_flags: + score += 0.20 + elif "has_website" in active_signals: + warnings.append("Keine E-Commerce-Flags — unklar ob Webshop vorhanden") + + # Tracking specified? (+0.15) + tracking_flags = {"uses_tracking", "uses_cookies_marketing", "strictly_necessary_cookies_only"} + if active_signals & tracking_flags: + score += 0.15 + else: + warnings.append("Keine Cookie/Tracking-Flags — Cookie-Banner-Pflicht unklar") + + # Products specified? (+0.15) + product_flags = {"sells_physical_products", "sells_digital_products", "sells_regulated_products"} + if active_signals & product_flags: + score += 0.15 + + # Enough signals overall? (+0.15) + if len(active_signals) >= 4: + score += 0.15 + elif len(active_signals) >= 2: + score += 0.10 + + # Documents found? (+0.15) + if len(required) >= 3: + score += 0.15 + elif len(required) >= 1: + score += 0.05 + + assessment.confidence = round(min(score, 1.0), 2) + + # Escalation detection + escalation_reasons = [] + + # E-commerce without distance_selling flag + if "has_ecommerce" in active_signals and "distance_selling" not in active_signals and "b2b_only" not in active_signals: + escalation_reasons.append( + "E-Commerce aktiv aber distance_selling/b2b_only nicht spezifiziert — " + "Widerrufsrecht-Pflicht unklar" + ) + + # Marketplace without payment clarification + if "operates_marketplace" in active_signals and "operates_payment_service" not in active_signals: + escalation_reasons.append( + "Marketplace aktiv — Pruefung ob eigene Zahlungsabwicklung oder externer PSP" + ) + + # Very few flags + if len(active_signals) < 2: + escalation_reasons.append( + "Zu wenige Flags fuer belastbare Dokumenten-Ableitung" + ) + + if escalation_reasons: + assessment.escalation_flag = True + assessment.escalation_reason = " | ".join(escalation_reasons) + assessment.confidence = min(assessment.confidence, 0.75) + + # Reasoning + parts = [] + parts.append(f"{len(active_signals)} Flags aktiv: {', '.join(sorted(active_signals))}") + parts.append(f"{len(required)} Pflichtdokumente, {len(recommended)} empfohlen") + + categories = set(d.category for d in required) + if categories: + parts.append(f"Kategorien: {', '.join(sorted(categories))}") + + if warnings: + parts.append(f"Hinweise: {'; '.join(warnings)}") + + if assessment.escalation_flag: + parts.append(f"ESKALATION: {assessment.escalation_reason}") + + assessment.reasoning = ". ".join(parts) + "." + assessment.warnings = warnings + + return assessment diff --git a/control-pipeline/tests/test_document_compliance.py b/control-pipeline/tests/test_document_compliance.py new file mode 100644 index 0000000..0688c3b --- /dev/null +++ b/control-pipeline/tests/test_document_compliance.py @@ -0,0 +1,190 @@ +"""Tests for Document Scope Resolver — required documents per business model.""" + +import pytest +from services.document_scope_resolver import resolve_required_documents + + +class TestBasicWebsite: + def test_only_website(self): + result = resolve_required_documents({"has_website": True}) + types = [d["document_type"] for d in result["required_documents"]] + assert "impressum" in types + assert "privacy_policy" in types + assert "withdrawal_policy" not in types + assert "terms_and_conditions" not in types + + def test_website_with_tracking(self): + result = resolve_required_documents({"has_website": True, "uses_tracking": True}) + types = [d["document_type"] for d in result["required_documents"]] + assert "cookie_banner" in types + assert "cookie_policy" in types + + def test_no_cookie_banner_without_tracking(self): + result = resolve_required_documents({"has_website": True}) + types = [d["document_type"] for d in result["required_documents"]] + assert "cookie_banner" not in types + + +class TestEcommerce: + def test_webshop_physical(self): + result = resolve_required_documents({ + "has_website": True, "has_ecommerce": True, + "sells_physical_products": True, "distance_selling": True, + "has_checkout": True, + }) + types = [d["document_type"] for d in result["required_documents"]] + assert "terms_and_conditions" in types + assert "withdrawal_policy" in types + assert "shipping_information" in types + assert "warranty_information" in types + assert "agb_checkout_summary" in types + assert "pricing_transparency" in types + assert "payment_terms" in types + + def test_b2b_only_no_withdrawal(self): + result = resolve_required_documents({ + "has_website": True, "has_ecommerce": True, + "sells_physical_products": True, "b2b_only": True, + "has_checkout": True, + }) + types = [d["document_type"] for d in result["required_documents"]] + assert "terms_and_conditions" in types + assert "withdrawal_policy" not in types # B2B = kein Widerruf + assert "return_policy" not in types + + def test_digital_products(self): + result = resolve_required_documents({ + "has_website": True, "has_ecommerce": True, + "sells_digital_products": True, "distance_selling": True, + }) + types = [d["document_type"] for d in result["required_documents"]] + assert "digital_content_terms" in types + assert "no_withdrawal_notice" in types + + +class TestSaaS: + def test_saas_basic(self): + result = resolve_required_documents({ + "has_website": True, "has_saas": True, "has_user_accounts": True, + }) + types = [d["document_type"] for d in result["required_documents"]] + assert "terms_of_service" in types + assert "data_processing_agreement" in types + assert "service_description" in types + + def test_saas_with_ai(self): + result = resolve_required_documents({ + "has_website": True, "has_saas": True, "uses_ai": True, + }) + types = [d["document_type"] for d in result["required_documents"]] + assert "ai_transparency_notice" in types + + def test_saas_with_automated_decisions(self): + result = resolve_required_documents({ + "has_website": True, "has_saas": True, "automated_decisions": True, + }) + types = [d["document_type"] for d in result["required_documents"]] + assert "automated_decision_explanation" in types + + +class TestHardware: + def test_regulated_products(self): + result = resolve_required_documents({ + "has_website": True, "has_ecommerce": True, + "sells_regulated_products": True, + }) + types = [d["document_type"] for d in result["required_documents"]] + assert "ce_conformity_declaration" in types + assert "product_safety_instructions" in types + + def test_electronics_weee(self): + result = resolve_required_documents({ + "has_website": True, "has_ecommerce": True, + "sells_electronics": True, + }) + types = [d["document_type"] for d in result["required_documents"]] + assert "weee_information" in types + + def test_battery_products(self): + result = resolve_required_documents({ + "has_website": True, "sells_physical_products": True, + "contains_battery": True, + }) + types = [d["document_type"] for d in result["required_documents"]] + assert "battery_disposal_information" in types + + +class TestMarketplace: + def test_marketplace(self): + result = resolve_required_documents({ + "has_website": True, "operates_marketplace": True, + }) + types = [d["document_type"] for d in result["required_documents"]] + assert "marketplace_seller_terms" in types + assert "marketplace_ranking_transparency" in types + + +class TestSubscription: + def test_subscription(self): + result = resolve_required_documents({ + "has_website": True, "has_saas": True, "subscription_model": True, + }) + types = [d["document_type"] for d in result["required_documents"]] + assert "subscription_cancellation_terms" in types + + +class TestAssessment: + def test_high_confidence_full_flags(self): + result = resolve_required_documents({ + "has_website": True, "has_ecommerce": True, + "uses_tracking": True, "sells_physical_products": True, + "distance_selling": True, + }) + assert result["assessment"]["confidence"] >= 0.85 + + def test_low_confidence_few_flags(self): + result = resolve_required_documents({"has_website": True}) + assert result["assessment"]["confidence"] < 0.80 + + def test_escalation_ecommerce_without_distance_selling(self): + result = resolve_required_documents({ + "has_website": True, "has_ecommerce": True, + }) + assert result["assessment"]["escalation_flag"] is True + + def test_no_escalation_b2b_explicit(self): + result = resolve_required_documents({ + "has_website": True, "has_ecommerce": True, "b2b_only": True, + }) + assert result["assessment"]["escalation_flag"] is False + + +class TestFullStack: + def test_full_stack_worst_case(self): + """Enterprise with everything: website + webshop + SaaS + AI + hardware.""" + result = resolve_required_documents({ + "has_website": True, "has_ecommerce": True, "has_saas": True, + "has_checkout": True, "distance_selling": True, + "uses_tracking": True, "uses_ai": True, + "sells_physical_products": True, "sells_regulated_products": True, + "sells_electronics": True, "contains_battery": True, + "subscription_model": True, "has_user_accounts": True, + }) + assert result["total_required"] >= 15 + types = [d["document_type"] for d in result["required_documents"]] + # Must have all base docs + assert "impressum" in types + assert "privacy_policy" in types + assert "cookie_banner" in types + # Must have ecommerce docs + assert "terms_and_conditions" in types + assert "withdrawal_policy" in types + # Must have SaaS docs + assert "terms_of_service" in types + assert "data_processing_agreement" in types + # Must have AI docs + assert "ai_transparency_notice" in types + # Must have hardware docs + assert "ce_conformity_declaration" in types + assert "weee_information" in types + assert "battery_disposal_information" in types