# mypy: disable-error-code="arg-type,assignment" # SQLAlchemy 1.x Column() descriptors are Column[T] statically, T at runtime. """ Banner admin service — site config + category + vendor CRUD. Phase 1 Step 4: extracted from ``compliance.api.banner_routes``. Covers the admin surface: site configs and the nested category and vendor collections. """ import uuid from datetime import datetime, timezone from typing import Any from sqlalchemy.orm import Session from compliance.db.banner_models import ( BannerCategoryConfigDB, BannerSiteConfigDB, BannerVendorConfigDB, ) from compliance.domain import ConflictError, NotFoundError, ValidationError from compliance.schemas.banner import ( CategoryConfigCreate, SiteConfigCreate, SiteConfigUpdate, VendorConfigCreate, ) from compliance.services._banner_serializers import ( category_to_dict, site_config_to_dict, vendor_to_dict, ) _UPDATABLE_SITE_FIELDS = ( "site_name", "site_url", "banner_title", "banner_description", "privacy_url", "imprint_url", "dsb_name", "dsb_email", "theme", "tcf_enabled", "is_active", ) class BannerAdminService: """Business logic for the banner admin surface.""" def __init__(self, db: Session) -> None: self.db = db # ------------------------------------------------------------------ # Internal lookups # ------------------------------------------------------------------ def _site_or_raise(self, tenant_id: uuid.UUID, site_id: str) -> BannerSiteConfigDB: config = ( self.db.query(BannerSiteConfigDB) .filter( BannerSiteConfigDB.tenant_id == tenant_id, BannerSiteConfigDB.site_id == site_id, ) .first() ) if not config: raise NotFoundError("Site config not found") return config @staticmethod def _parse_uuid(raw: str, label: str) -> uuid.UUID: try: return uuid.UUID(raw) except ValueError as exc: raise ValidationError(f"Invalid {label} ID") from exc # ------------------------------------------------------------------ # Site configs # ------------------------------------------------------------------ def list_sites(self, tenant_id: str) -> list[dict[str, Any]]: tid = uuid.UUID(tenant_id) configs = ( self.db.query(BannerSiteConfigDB) .filter(BannerSiteConfigDB.tenant_id == tid) .order_by(BannerSiteConfigDB.created_at.desc()) .all() ) return [site_config_to_dict(c) for c in configs] def create_site(self, tenant_id: str, body: SiteConfigCreate) -> dict[str, Any]: tid = uuid.UUID(tenant_id) existing = ( self.db.query(BannerSiteConfigDB) .filter( BannerSiteConfigDB.tenant_id == tid, BannerSiteConfigDB.site_id == body.site_id, ) .first() ) if existing: raise ConflictError( f"Site config for '{body.site_id}' already exists" ) config = BannerSiteConfigDB( tenant_id=tid, site_id=body.site_id, site_name=body.site_name, site_url=body.site_url, banner_title=body.banner_title or "Cookie-Einstellungen", banner_description=body.banner_description, privacy_url=body.privacy_url, imprint_url=body.imprint_url, dsb_name=body.dsb_name, dsb_email=body.dsb_email, theme=body.theme or {}, tcf_enabled=body.tcf_enabled, ) self.db.add(config) self.db.commit() self.db.refresh(config) return site_config_to_dict(config) def update_site( self, tenant_id: str, site_id: str, body: SiteConfigUpdate ) -> dict[str, Any]: tid = uuid.UUID(tenant_id) config = self._site_or_raise(tid, site_id) for field in _UPDATABLE_SITE_FIELDS: val = getattr(body, field, None) if val is not None: setattr(config, field, val) config.updated_at = datetime.now(timezone.utc) self.db.commit() self.db.refresh(config) return site_config_to_dict(config) def delete_site(self, tenant_id: str, site_id: str) -> None: tid = uuid.UUID(tenant_id) config = self._site_or_raise(tid, site_id) self.db.delete(config) self.db.commit() # ------------------------------------------------------------------ # Categories # ------------------------------------------------------------------ def list_categories(self, tenant_id: str, site_id: str) -> list[dict[str, Any]]: tid = uuid.UUID(tenant_id) config = self._site_or_raise(tid, site_id) cats = ( self.db.query(BannerCategoryConfigDB) .filter(BannerCategoryConfigDB.site_config_id == config.id) .order_by(BannerCategoryConfigDB.sort_order) .all() ) return [category_to_dict(c) for c in cats] def create_category( self, tenant_id: str, site_id: str, body: CategoryConfigCreate ) -> dict[str, Any]: tid = uuid.UUID(tenant_id) config = self._site_or_raise(tid, site_id) cat = BannerCategoryConfigDB( site_config_id=config.id, category_key=body.category_key, name_de=body.name_de, name_en=body.name_en, description_de=body.description_de, description_en=body.description_en, is_required=body.is_required, sort_order=body.sort_order, ) self.db.add(cat) self.db.commit() self.db.refresh(cat) return category_to_dict(cat) def delete_category(self, category_id: str) -> None: cid = self._parse_uuid(category_id, "category") cat = ( self.db.query(BannerCategoryConfigDB) .filter(BannerCategoryConfigDB.id == cid) .first() ) if not cat: raise NotFoundError("Category not found") self.db.delete(cat) self.db.commit() # ------------------------------------------------------------------ # Vendors # ------------------------------------------------------------------ def list_vendors(self, tenant_id: str, site_id: str) -> list[dict[str, Any]]: tid = uuid.UUID(tenant_id) config = self._site_or_raise(tid, site_id) vendors = ( self.db.query(BannerVendorConfigDB) .filter(BannerVendorConfigDB.site_config_id == config.id) .all() ) return [vendor_to_dict(v) for v in vendors] def create_vendor( self, tenant_id: str, site_id: str, body: VendorConfigCreate ) -> dict[str, Any]: tid = uuid.UUID(tenant_id) config = self._site_or_raise(tid, site_id) vendor = BannerVendorConfigDB( site_config_id=config.id, vendor_name=body.vendor_name, vendor_url=body.vendor_url, category_key=body.category_key, description_de=body.description_de, description_en=body.description_en, cookie_names=body.cookie_names, retention_days=body.retention_days, ) self.db.add(vendor) self.db.commit() self.db.refresh(vendor) return vendor_to_dict(vendor) def delete_vendor(self, vendor_id: str) -> None: vid = self._parse_uuid(vendor_id, "vendor") vendor = ( self.db.query(BannerVendorConfigDB) .filter(BannerVendorConfigDB.id == vid) .first() ) if not vendor: raise NotFoundError("Vendor not found") self.db.delete(vendor) self.db.commit()