diff --git a/admin-compliance/app/sdk/document-generator/page.tsx b/admin-compliance/app/sdk/document-generator/page.tsx index 986fbb7..aef91cc 100644 --- a/admin-compliance/app/sdk/document-generator/page.tsx +++ b/admin-compliance/app/sdk/document-generator/page.tsx @@ -101,6 +101,78 @@ function DocumentGeneratorPageInner() { } }, [state?.complianceScope?.determinedLevel, state?.companyProfile]) + // ── MODULE WIRING: CookieBanner → CONSENT + FEATURES ───────────────────── + useEffect(() => { + const banner = state?.cookieBanner + if (!banner) return + const cats = banner.categories || [] + const analyticsTools = cats + .filter((c) => c.id === 'analytics' || c.id === 'statistics') + .flatMap((c) => c.cookies?.map((ck) => ck.name) ?? []) + const marketingTools = cats + .filter((c) => c.id === 'marketing') + .flatMap((c) => c.cookies?.map((ck) => ck.name) ?? []) + const hasFunctional = cats.some((c) => c.id === 'functional') + + setContext((prev) => ({ + ...prev, + CONSENT: { + ...prev.CONSENT, + ANALYTICS_TOOLS: analyticsTools.length > 0 ? analyticsTools.join(', ') : prev.CONSENT.ANALYTICS_TOOLS, + MARKETING_PARTNERS: marketingTools.length > 0 ? marketingTools.join(', ') : prev.CONSENT.MARKETING_PARTNERS, + }, + FEATURES: { + ...prev.FEATURES, + CMP_NAME: 'BreakPilot CMP', + CMP_LOGS_CONSENTS: true, + HAS_FUNCTIONAL_COOKIES: hasFunctional || prev.FEATURES.HAS_FUNCTIONAL_COOKIES, + CONSENT_WITHDRAWAL_PATH: 'Footer-Link "Cookie-Einstellungen"', + }, + })) + }, [state?.cookieBanner]) + + // ── MODULE WIRING: Loeschfristen → PRIVACY retention ────────────────────── + useEffect(() => { + const policies = state?.retentionPolicies + if (!policies || policies.length === 0) return + const maxMonths = policies.reduce((max, p) => { + const match = p.retentionPeriod?.match(/(\d+)\s*(Monat|Jahr|Tag)/i) + if (!match) return max + const val = parseInt(match[1], 10) + const unit = match[2].toLowerCase() + const months = unit.startsWith('jahr') ? val * 12 : unit.startsWith('tag') ? Math.ceil(val / 30) : val + return Math.max(max, months) + }, 0) + if (maxMonths > 0) { + setContext((prev) => ({ + ...prev, + PRIVACY: { ...prev.PRIVACY, ANALYTICS_RETENTION_MONTHS: maxMonths }, + })) + } + }, [state?.retentionPolicies]) + + // ── MODULE WIRING: UseCases → FEATURES flags ───────────────────────────── + useEffect(() => { + const useCases = state?.useCases + if (!useCases || useCases.length === 0) return + const allText = useCases.map((uc) => `${uc.name} ${uc.description}`).join(' ').toLowerCase() + const hasAccount = allText.includes('account') || allText.includes('konto') || allText.includes('registrier') + const hasPayments = allText.includes('zahlung') || allText.includes('payment') || allText.includes('stripe') || allText.includes('paypal') + const hasNewsletter = allText.includes('newsletter') || allText.includes('mailchimp') || allText.includes('e-mail-marketing') + const hasSocial = allText.includes('social') || allText.includes('linkedin') || allText.includes('facebook') || allText.includes('instagram') + + setContext((prev) => ({ + ...prev, + FEATURES: { + ...prev.FEATURES, + HAS_ACCOUNT: hasAccount || prev.FEATURES.HAS_ACCOUNT, + HAS_PAYMENTS: hasPayments || prev.FEATURES.HAS_PAYMENTS, + HAS_NEWSLETTER: hasNewsletter || prev.FEATURES.HAS_NEWSLETTER, + HAS_SOCIAL_MEDIA: hasSocial || prev.FEATURES.HAS_SOCIAL_MEDIA, + }, + })) + }, [state?.useCases]) + // Pre-fill extra placeholders from Einwilligungen data points useEffect(() => { if (selectedDataPointsData && selectedDataPointsData.length > 0) { diff --git a/backend-compliance/compliance/db/banner_models.py b/backend-compliance/compliance/db/banner_models.py index a343646..2c96981 100644 --- a/backend-compliance/compliance/db/banner_models.py +++ b/backend-compliance/compliance/db/banner_models.py @@ -36,6 +36,20 @@ class BannerConsentDB(Base): user_agent = Column(Text) consent_string = Column(Text) linked_email = Column(Text) + # Vendor-agnostische Felder (Migration 107) + consent_method = Column(Text) # accept_all / reject_all / custom_selection + banner_version = Column(Integer) + banner_config_hash = Column(Text) + geo_country = Column(Text) + geo_region = Column(Text) + consent_scope = Column(Text, default='domain') + page_url = Column(Text) + referrer = Column(Text) + device_type = Column(Text) # mobile / desktop / tablet + browser = Column(Text) + os = Column(Text) + screen_resolution = Column(Text) + session_id = Column(Text) expires_at = Column(DateTime) created_at = Column(DateTime, nullable=False, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) @@ -66,6 +80,8 @@ class BannerConsentAuditLogDB(Base): user_agent = Column(Text) banner_config_hash = Column(Text) consent_version = Column(Integer) + consent_method = Column(Text) + page_url = Column(Text) created_at = Column(DateTime, nullable=False, default=datetime.utcnow) __table_args__ = ( diff --git a/backend-compliance/compliance/services/_banner_serializers.py b/backend-compliance/compliance/services/_banner_serializers.py index d7df8b5..7175f43 100644 --- a/backend-compliance/compliance/services/_banner_serializers.py +++ b/backend-compliance/compliance/services/_banner_serializers.py @@ -25,8 +25,22 @@ def consent_to_dict(c: BannerConsentDB) -> dict[str, Any]: "vendors": c.vendors or [], "vendor_consents": c.vendor_consents or {}, "ip_hash": c.ip_hash, + "user_agent": c.user_agent, "consent_string": c.consent_string, "linked_email": c.linked_email, + "consent_method": c.consent_method, + "banner_version": c.banner_version, + "banner_config_hash": c.banner_config_hash, + "geo_country": c.geo_country, + "geo_region": c.geo_region, + "consent_scope": c.consent_scope, + "page_url": c.page_url, + "referrer": c.referrer, + "device_type": c.device_type, + "browser": c.browser, + "os": c.os, + "screen_resolution": c.screen_resolution, + "session_id": c.session_id, "expires_at": c.expires_at.isoformat() if c.expires_at else None, "created_at": c.created_at.isoformat() if c.created_at else None, "updated_at": c.updated_at.isoformat() if c.updated_at else None, diff --git a/backend-compliance/compliance/services/banner_consent_service.py b/backend-compliance/compliance/services/banner_consent_service.py index 51dffb8..5f2c360 100644 --- a/backend-compliance/compliance/services/banner_consent_service.py +++ b/backend-compliance/compliance/services/banner_consent_service.py @@ -75,6 +75,9 @@ class BannerConsentService: consent_version: Optional[int] = None, vendor_consents: Optional[dict[str, bool]] = None, user_agent: Optional[str] = None, + *, + consent_method: Optional[str] = None, + page_url: Optional[str] = None, ) -> None: entry = BannerConsentAuditLogDB( tenant_id=tenant_id, @@ -88,6 +91,8 @@ class BannerConsentService: user_agent=user_agent, banner_config_hash=banner_config_hash, consent_version=consent_version, + consent_method=consent_method, + page_url=page_url, ) self.db.add(entry) @@ -166,6 +171,16 @@ class BannerConsentService: user_agent: Optional[str], consent_string: Optional[str], vendor_consents: Optional[dict[str, bool]] = None, + *, + consent_method: Optional[str] = None, + page_url: Optional[str] = None, + referrer: Optional[str] = None, + device_type: Optional[str] = None, + browser: Optional[str] = None, + os: Optional[str] = None, + screen_resolution: Optional[str] = None, + session_id: Optional[str] = None, + consent_scope: Optional[str] = None, ) -> dict[str, Any]: """Upsert a device consent row for (tenant, site, device_fingerprint). @@ -185,6 +200,21 @@ class BannerConsentService: if not consent_string: consent_string = self._maybe_generate_tc_string(tid, site_id, categories) + # Vendor-agnostische Zusatzfelder + extra = { + "consent_method": consent_method, + "banner_version": config_ver, + "banner_config_hash": config_hash, + "page_url": page_url, + "referrer": referrer, + "device_type": device_type, + "browser": browser, + "os": os, + "screen_resolution": screen_resolution, + "session_id": session_id, + "consent_scope": consent_scope or "domain", + } + existing = ( self.db.query(BannerConsentDB) .filter( @@ -204,11 +234,14 @@ class BannerConsentService: existing.consent_string = consent_string existing.expires_at = expires_at existing.updated_at = now + for key, val in extra.items(): + setattr(existing, key, val) self.db.flush() self._log( tid, existing.id, "consent_updated", site_id, device_fingerprint, categories, ip_hash, config_hash, config_ver, vendor_consents=vendor_consents, user_agent=user_agent, + consent_method=consent_method, page_url=page_url, ) self.db.commit() self.db.refresh(existing) @@ -225,6 +258,7 @@ class BannerConsentService: user_agent=user_agent, consent_string=consent_string, expires_at=expires_at, + **extra, ) self.db.add(consent) self.db.flush() @@ -232,6 +266,7 @@ class BannerConsentService: tid, consent.id, "consent_given", site_id, device_fingerprint, categories, ip_hash, config_hash, config_ver, vendor_consents=vendor_consents, user_agent=user_agent, + consent_method=consent_method, page_url=page_url, ) self.db.commit() self.db.refresh(consent) @@ -369,6 +404,24 @@ class BannerConsentService: ], } + def list_consents( + self, tenant_id: str, site_id: Optional[str] = None, + limit: int = 50, offset: int = 0, + ) -> dict[str, Any]: + """List paginated banner consents for admin dashboard.""" + tid = uuid.UUID(tenant_id) + base = self.db.query(BannerConsentDB).filter(BannerConsentDB.tenant_id == tid) + if site_id: + base = base.filter(BannerConsentDB.site_id == site_id) + total = base.count() + rows = base.order_by(BannerConsentDB.created_at.desc()).offset(offset).limit(limit).all() + return { + "consents": [consent_to_dict(c) for c in rows], + "total": total, + "limit": limit, + "offset": offset, + } + def get_site_stats(self, tenant_id: str, site_id: str) -> dict[str, Any]: """Compute consent count + per-category acceptance rates for a site.""" tid = uuid.UUID(tenant_id)