# WCO-K03: Theme preset system
**Area:** Widget Customization Docs
**Model:** moonshotai/kimi-k2.5
**Tokens:** 8,362
**Cost:** $0.0089
**Time:** 2203ms
**Timestamp:** 2026-02-16T13:54:11.833418

---

I'll build a complete theme preset system for Sunaiva with database persistence, validation, caching, and shadow DOM integration.

## 1. Database Models & Schemas

```python
# app/models/theme.py
from datetime import datetime
from typing import Optional, Dict, Any
from sqlalchemy import Column, String, DateTime, Boolean, JSON, Integer, Index
from sqlalchemy.dialects.postgresql import UUID, JSONB
import uuid
from app.core.database import Base


class Theme(Base):
    """Database model for widget themes."""
    
    __tablename__ = "themes"
    
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    name = Column(String(100), nullable=False, index=True)
    description = Column(String(500), nullable=True)
    category = Column(String(50), nullable=False, index=True)  # 'preset', 'custom', 'imported'
    trade_type = Column(String(50), nullable=True)  # 'plumber', 'electrician', etc.
    
    # Core theme properties stored as JSONB for flexibility
    colors = Column(JSONB, nullable=False, default=dict)
    typography = Column(JSONB, nullable=False, default=dict)
    spacing = Column(JSONB, nullable=False, default=dict)
    effects = Column(JSONB, nullable=False, default=dict)
    layout = Column(JSONB, nullable=False, default=dict)
    
    # Metadata
    is_preset = Column(Boolean, default=False, nullable=False, index=True)
    is_active = Column(Boolean, default=False, nullable=False, index=True)
    version = Column(String(10), default="1.0.0", nullable=False)
    business_id = Column(UUID(as_uuid=True), nullable=True, index=True)
    
    created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
    created_by = Column(UUID(as_uuid=True), nullable=True)
    
    # Indexes for performance
    __table_args__ = (
        Index('idx_themes_business_active', 'business_id', 'is_active'),
        Index('idx_themes_preset_category', 'is_preset', 'category'),
    )
    
    def to_dict(self) -> Dict[str, Any]:
        """Convert model to dictionary."""
        return {
            "id": str(self.id),
            "name": self.name,
            "description": self.description,
            "category": self.category,
            "trade_type": self.trade_type,
            "colors": self.colors,
            "typography": self.typography,
            "spacing": self.spacing,
            "effects": self.effects,
            "layout": self.layout,
            "is_preset": self.is_preset,
            "is_active": self.is_active,
            "version": self.version,
            "business_id": str(self.business_id) if self.business_id else None,
            "created_at": self.created_at.isoformat() if self.created_at else None,
            "updated_at": self.updated_at.isoformat() if self.updated_at else None,
        }
```

```python
# app/schemas/theme.py
from datetime import datetime
from typing import Optional, Dict, Any, List
from pydantic import BaseModel, Field, validator, root_validator
import re


class ColorPalette(BaseModel):
    """Color palette schema with validation."""
    primary: str = Field(..., regex=r"^#[0-9A-Fa-f]{6}$")
    secondary: str = Field(..., regex=r"^#[0-9A-Fa-f]{6}$")
    background: str = Field(..., regex=r"^#[0-9A-Fa-f]{6}$")
    surface: str = Field(..., regex=r"^#[0-9A-Fa-f]{6}$")
    text: str = Field(..., regex=r"^#[0-9A-Fa-f]{6}$")
    text_secondary: str = Field(..., regex=r"^#[0-9A-Fa-f]{6}$")
    accent: str = Field(..., regex=r"^#[0-9A-Fa-f]{6}$")
    error: str = Field(default="#DC2626", regex=r"^#[0-9A-Fa-f]{6}$")
    success: str = Field(default="#16A34A", regex=r"^#[0-9A-Fa-f]{6}$")
    warning: str = Field(default="#F59E0B", regex=r"^#[0-9A-Fa-f]{6}$")
    info: str = Field(default="#3B82F6", regex=r"^#[0-9A-Fa-f]{6}$")
    
    @validator("*")
    def validate_hex_contrast(cls, v):
        """Ensure hex values are uppercase for consistency."""
        return v.upper()


class Typography(BaseModel):
    """Typography configuration."""
    font_family: str = Field(default="Inter, system-ui, sans-serif", max_length=200)
    font_family_heading: Optional[str] = Field(default=None, max_length=200)
    font_size_base: str = Field(default="16px", regex=r"^\d+(px|rem|em)$")
    font_size_small: str = Field(default="14px", regex=r"^\d+(px|rem|em)$")
    font_size_large: str = Field(default="18px", regex=r"^\d+(px|rem|em)$")
    font_size_h1: str = Field(default="2rem", regex=r"^\d+(\.\d+)?(px|rem|em)$")
    font_size_h2: str = Field(default="1.5rem", regex=r"^\d+(\.\d+)?(px|rem|em)$")
    line_height: float = Field(default=1.5, ge=1.0, le=2.5)
    font_weight_normal: int = Field(default=400, ge=100, le=900)
    font_weight_bold: int = Field(default=700, ge=100, le=900)


class Spacing(BaseModel):
    """Spacing scale configuration."""
    xs: str = Field(default="0.25rem", regex=r"^\d+(\.\d+)?(px|rem|em)$")
    sm: str = Field(default="0.5rem", regex=r"^\d+(\.\d+)?(px|rem|em)$")
    md: str = Field(default="1rem", regex=r"^\d+(\.\d+)?(px|rem|em)$")
    lg: str = Field(default="1.5rem", regex=r"^\d+(\.\d+)?(px|rem|em)$")
    xl: str = Field(default="2rem", regex=r"^\d+(\.\d+)?(px|rem|em)$")
    xxl: str = Field(default="3rem", regex=r"^\d+(\.\d+)?(px|rem|em)$")


class Effects(BaseModel):
    """Visual effects configuration."""
    border_radius_sm: str = Field(default="4px", regex=r"^\d+(px|rem)$")
    border_radius_md: str = Field(default="8px", regex=r"^\d+(px|rem)$")
    border_radius_lg: str = Field(default="12px", regex=r"^\d+(px|rem)$")
    border_radius_full: str = Field(default="9999px", regex=r"^\d+(px|rem)$")
    shadow_sm: str = Field(default="0 1px 2px 0 rgba(0, 0, 0, 0.05)", max_length=200)
    shadow_md: str = Field(default="0 4px 6px -1px rgba(0, 0, 0, 0.1)", max_length=200)
    shadow_lg: str = Field(default="0 10px 15px -3px rgba(0, 0, 0, 0.1)", max_length=200)
    transition_speed: str = Field(default="200ms", regex=r"^\d+(ms|s)$")
    transition_easing: str = Field(default="cubic-bezier(0.4, 0, 0.2, 1)", max_length=100)


class Layout(BaseModel):
    """Widget layout configuration."""
    widget_width: str = Field(default="380px", regex=r"^\d+(px|rem|%)$")
    widget_height: str = Field(default="600px", regex=r"^\d+(px|rem|%)$")
    header_height: str = Field(default="60px", regex=r"^\d+(px|rem)$")
    bubble_size: str = Field(default="56px", regex=r"^\d+(px|rem)$")
    max_content_width: str = Field(default="100%", regex=r"^\d+(px|rem|%)$")
    container_padding: str = Field(default="1rem", regex=r"^\d+(\.\d+)?(px|rem|em)$")


class ThemeBase(BaseModel):
    """Base theme schema."""
    name: str = Field(..., min_length=1, max_length=100)
    description: Optional[str] = Field(default=None, max_length=500)
    category: str = Field(default="custom", regex=r"^(preset|custom|imported)$")
    trade_type: Optional[str] = Field(default=None, max_length=50)
    colors: ColorPalette
    typography: Typography = Field(default_factory=Typography)
    spacing: Spacing = Field(default_factory=Spacing)
    effects: Effects = Field(default_factory=Effects)
    layout: Layout = Field(default_factory=Layout)


class ThemeCreate(ThemeBase):
    """Schema for creating a theme."""
    business_id: Optional[str] = None


class ThemeUpdate(BaseModel):
    """Schema for updating a theme."""
    name: Optional[str] = Field(default=None, min_length=1, max_length=100)
    description: Optional[str] = Field(default=None, max_length=500)
    colors: Optional[ColorPalette] = None
    typography: Optional[Typography] = None
    spacing: Optional[Spacing] = None
    effects: Optional[Effects] = None
    layout: Optional[Layout] = None


class ThemeResponse(ThemeBase):
    """Schema for theme responses."""
    id: str
    is_preset: bool
    is_active: bool
    version: str
    business_id: Optional[str]
    created_at: datetime
    updated_at: datetime
    
    class Config:
        orm_mode = True


class ThemeExport(BaseModel):
    """Schema for theme export/import."""
    schema_version: str = "1.0.0"
    exported_at: datetime
    theme: ThemeResponse
    
    @validator("schema_version")
    def validate_version(cls, v):
        if not re.match(r"^\d+\.\d+\.\d+$", v):
            raise ValueError("Invalid semantic version format")
        return v


class ThemeApplyRequest(BaseModel):
    """Schema for applying a theme."""
    theme_id: str
    business_id: str


class CSSVariableMapping(BaseModel):
    """Mapping of theme properties to CSS variables."""
    mapping: Dict[str, str] = Field(default_factory=dict)
    
    class Config:
        schema_extra = {
            "example": {
                "--sunaiva-primary": "colors.primary",
                "--sunaiva-font-family": "typography.font_family"
            }
        }
```

## 2. Service Layer with Presets

```python
# app/services/theme_service.py
import json
import logging
from typing import List, Optional, Dict, Any
from datetime import datetime
from uuid import UUID, uuid4

from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update, delete, and_
from sqlalchemy.dialects.postgresql import insert
from redis.asyncio import Redis

from app.models.theme import Theme
from app.schemas.theme import (
    ThemeCreate, ThemeUpdate, ThemeExport, ThemeResponse,
    ColorPalette, Typography, Spacing, Effects, Layout
)
from app.core.exceptions import ThemeNotFoundError, ThemeValidationError, DuplicateThemeError

logger = logging.getLogger(__name__)


class ThemeService:
    """Service for managing widget themes."""
    
    # 10 Pre-built trade themes
    PRESET_THEMES = [
        {
            "name": "Plumber Pro Blue",
            "description": "Professional blue theme for plumbing services",
            "trade_type": "plumber",
            "category": "preset",
            "colors": {
                "primary": "#0066CC",
                "secondary": "#00A8E8",
                "background": "#F0F9FF",
                "surface": "#FFFFFF",
                "text": "#1E293B",
                "text_secondary": "#64748B",
                "accent": "#0EA5E9",
                "error": "#DC2626",
                "success": "#16A34A",
                "warning": "#F59E0B",
                "info": "#3B82F6"
            },
            "typography": {
                "font_family": "Inter, system-ui, -apple-system, sans-serif",
                "font_family_heading": "Poppins, sans-serif",
                "font_size_base": "16px",
                "font_size_small": "14px",
                "font_size_large": "18px",
                "font_size_h1": "1.75rem",
                "font_size_h2": "1.25rem",
                "line_height": 1.6,
                "font_weight_normal": 400,
                "font_weight_bold": 600
            },
            "effects": {
                "border_radius_sm": "4px",
                "border_radius_md": "8px",
                "border_radius_lg": "12px",
                "border_radius_full": "9999px",
                "shadow_sm": "0 1px 2px 0 rgba(0, 102, 204, 0.05)",
                "shadow_md": "0 4px 6px -1px rgba(0, 102, 204, 0.1)",
                "shadow_lg": "0 10px 15px -3px rgba(0, 102, 204, 0.15)",
                "transition_speed": "200ms",
                "transition_easing": "cubic-bezier(0.4, 0, 0.2, 1)"
            }
        },
        {
            "name": "Electrician High-Viz",
            "description": "Safety yellow theme for electrical contractors",
            "trade_type": "electrician",
            "category": "preset",
            "colors": {
                "primary": "#FACC15",
                "secondary": "#171717",
                "background": "#FFFBEB",
                "surface": "#FFFFFF",
                "text": "#171717",
                "text_secondary": "#525252",
                "accent": "#EAB308",
                "error": "#DC2626",
                "success": "#16A34A",
                "warning": "#EA580C",
                "info": "#2563EB"
            },
            "typography": {
                "font_family": "Roboto, system-ui, sans-serif",
                "font_family_heading": "Roboto Condensed, sans-serif",
                "font_size_base": "16px",
                "font_size_small": "14px",
                "font_size_large": "18px",
                "font_size_h1": "1.875rem",
                "font_size_h2": "1.5rem",
                "line_height": 1.5,
                "font_weight_normal": 400,
                "font_weight_bold": 700
            },
            "effects": {
                "border_radius_sm": "2px",
                "border_radius_md": "4px",
                "border_radius_lg": "8px",
                "border_radius_full": "9999px",
                "shadow_sm": "0 1px 2px 0 rgba(0, 0, 0, 0.1)",
                "shadow_md": "0 4px 6px -1px rgba(0, 0, 0, 0.15)",
                "shadow_lg": "0 10px 15px -3px rgba(0, 0, 0, 0.2)",
                "transition_speed": "150ms",
                "transition_easing": "ease-out"
            }
        },
        {
            "name": "Builder Orange",
            "description": "Rugged orange theme for construction and building",
            "trade_type": "builder",
            "category": "preset",
            "colors": {
                "primary": "#EA580C",
                "secondary": "#7C2D12",
                "background": "#FFF7ED",
                "surface": "#FFFFFF",
                "text": "#431407",
                "text_secondary": "#9A3412",
                "accent": "#C2410C",
                "error": "#DC2626",
                "success": "#15803D",
                "warning": "#CA8A04",
                "info": "#0369A1"
            },
            "typography": {
                "font_family": "Oswald, Arial, sans-serif",
                "font_family_heading": "Oswald, Arial Narrow, sans-serif",
                "font_size_base": "16px",
                "font_size_small": "14px",
                "font_size_large": "18px",
                "font_size_h1": "2rem",
                "font_size_h2": "1.5rem",
                "line_height": 1.4,
                "font_weight_normal": 400,
                "font_weight_bold": 600
            }
        },
        {
            "name": "HVAC Cool Teal",
            "description": "Cool, calming teal for HVAC professionals",
            "trade_type": "hvac",
            "category": "preset",
            "colors": {
                "primary": "#0D9488",
                "secondary": "#134E4A",
                "background": "#F0FDFA",
                "surface": "#FFFFFF",
                "text": "#042F2E",
                "text_secondary": "#115E59",
                "accent": "#14B8A6",
                "error": "#DC2626",
                "success": "#059669",
                "warning": "#D97706",
                "info": "#0891B2"
            }
        },
        {
            "name": "Painter Fresh",
            "description": "Clean white with colorful accents for painters",
            "trade_type": "painter",
            "category": "preset",
            "colors": {
                "primary": "#EC4899",
                "secondary": "#8B5CF6",
                "background": "#FAFAFA",
                "surface": "#FFFFFF",
                "text": "#18181B",
                "text_secondary": "#71717A",
                "accent": "#F472B6",
                "error": "#EF4444",
                "success": "#22C55E",
                "warning": "#F97316",
                "info": "#3B82F6"
            }
        },
        {
            "name": "Landscaper Earth",
            "description": "Natural greens and earth tones for landscaping",
            "trade_type": "landscaper",
            "category": "preset",
            "colors": {
                "primary": "#15803D",
                "secondary": "#3F6212",
                "background": "#F7FEE7",
                "surface": "#FFFFFF",
                "text": "#14532D",
                "text_secondary": "#4D7C0F",
                "accent": "#65A30D",
                "error": "#DC2626",
                "success": "#15803D",
                "warning": "#A16207",
                "info": "#0369A1"
            }
        },
        {
            "name": "Roofer Red",
            "description": "Bold red theme for roofing professionals",
            "trade_type": "roofer",
            "category": "preset",
            "colors": {
                "primary": "#DC2626",
                "secondary": "#7F1D1D",
                "background": "#FEF2F2",
                "surface": "#FFFFFF",
                "text": "#450A0A",
                "text_secondary": "#991B1B",
                "accent": "#B91C1C",
                "error": "#DC2626",
                "success": "#16A34A",
                "warning": "#F59E0B",
                "info": "#2563EB"
            }
        },
        {
            "name": "Carpenter Wood",
            "description": "Warm wood tones for carpentry services",
            "trade_type": "carpenter",
            "category": "preset",
            "colors": {
                "primary": "#92400E",
                "secondary": "#78350F",
                "background": "#FFF7ED",
                "surface": "#FFFBF5",
                "text": "#451A03",
                "text_secondary": "#9A3412",
                "accent": "#B45309",
                "error": "#DC2626",
                "success": "#15803D",
                "warning": "#D97706",
                "info": "#0369A1"
            }
        },
        {
            "name": "Mechanic Dark",
            "description": "Dark theme with neon accents for auto mechanics",
            "trade_type": "mechanic",
            "category": "preset",
            "colors": {
                "primary": "#06B6D4",
                "secondary": "#0891B2",
                "background": "#0F172A",
                "surface": "#1E293B",
                "text": "#F8FAFC",
                "text_secondary": "#94A3B8",
                "accent": "#22D3EE",
                "error": "#F87171",
                "success": "#4ADE80",
                "warning": "#FBBF24",
                "info": "#60A5FA"
            },
            "typography": {
                "font_family": "Rajdhani, sans-serif",
                "font_family_heading": "Orbitron, sans-serif",
                "font_size_base": "16px",
                "font_size_small": "14px",
                "font_size_large": "18px",
                "font_size_h1": "1.75rem",
                "font_size_h2": "1.25rem",
                "line_height": 1.5,
                "font_weight_normal": 400,
                "font_weight_bold": 600
            }
        },
        {
            "name": "Cleaner Mint",
            "description": "Fresh mint and white for cleaning services",
            "trade_type": "cleaner",
            "category": "preset",
            "colors": {
                "primary": "#10B981",
                "secondary": "#059669",
                "background": "#ECFDF5",
                "surface": "#FFFFFF",
                "text": "#064E3B",
                "text_secondary": "#047857",
                "accent": "#34D399",
                "error": "#DC2626",
                "success": "#059669",
                "warning": "#D97706",
                "info": "#0891B2"
            }
        }
    ]
    
    def __init__(self, db: AsyncSession, redis: Redis):
        self.db = db
        self.redis = redis
        self.cache_prefix = "sunaiva:theme:"
        self.cache_ttl = 3600  # 1 hour
    
    async def initialize_presets(self) -> None:
        """Initialize preset themes in database if they don't exist."""
        try:
            for preset in self.PRESET_THEMES:
                # Check if preset exists
                stmt = select(Theme).where(
                    and_(
                        Theme.is_preset == True,
                        Theme.name == preset["name"]
                    )
                )
                result = await self.db.execute(stmt)
                existing = result.scalar_one_or_none()
                
                if not existing:
                    theme_data = {
                        "id": uuid4(),
                        "name": preset["name"],
                        "description": preset.get("description"),
                        "category": "preset",
                        "trade_type": preset.get("trade_type"),
                        "is_preset": True,
                        "is_active": False,
                        "version": "1.0.0",
                        "colors": preset.get("colors", {}),
                        "typography": preset.get("typography", {}),
                        "spacing": preset.get("spacing", {}),
                        "effects": preset.get("effects", {}),
                        "layout": preset.get("layout", {}),
                        "created_at": datetime.utcnow(),
                        "updated_at": datetime.utcnow()
                    }
                    
                    theme = Theme(**theme_data)
                    self.db.add(theme)
                    logger.info(f"Created preset theme: {preset['name']}")
            
            await self.db.commit()
            
        except Exception as e:
            await self.db.rollback()
            logger.error(f"Failed to initialize presets: {str(e)}")
            raise
    
    async def create_theme(self, theme_data: ThemeCreate, user_id: Optional[str] = None) -> Theme:
        """Create a new custom theme."""
        try:
            # Check for duplicate names within business
            if theme_data.business_id:
                stmt = select(Theme).where(
                    and_(
                        Theme.name == theme_data.name,
                        Theme.business_id == UUID(theme_data.business_id)
                    )
                )
                result = await self.db.execute(stmt)
                if result.scalar_one_or_none():
                    raise DuplicateThemeError(f"Theme '{theme_data.name}' already exists")
            
            theme = Theme(
                id=uuid4(),
                name=theme_data.name,
                description=theme_data.description,
                category=theme_data.category,
                trade_type=theme_data.trade_type,
                colors=theme_data.colors.dict(),
                typography=theme_data.typography.dict(),
                spacing=theme_data.spacing.dict(),
                effects=theme_data.effects.dict(),
                layout=theme_data.layout.dict(),
                is_preset=False,
                is_active=False,
                version="1.0.0",
                business_id=UUID(theme_data.business_id) if theme_data.business_id else None,
                created_by=UUID(user_id) if user_id else None,
                created_at=datetime.utcnow(),
                updated_at=datetime.utcnow()
            )
            
            self.db.add(theme)
            await self.db.commit()
            await self.db.refresh(theme)
            
            # Invalidate cache
            await self._invalidate_cache(theme.id)
            
            return theme
            
        except DuplicateThemeError:
            raise
        except Exception as e:
            await self.db.rollback()
            logger.error(f"Failed to create theme: {str(e)}")
            raise ThemeValidationError(f"Failed to create theme: {str(e)}")
    
    async def get_theme(self, theme_id: str) -> Theme:
        """Get theme by ID with caching."""
        cache_key = f"{self.cache_prefix}{theme_id}"
        
        try:
            # Try cache first
            cached = await self.redis.get(cache_key)
            if cached:
                data = json.loads(cached)
                return Theme(**data)
            
            # Fetch from DB
            stmt = select(Theme).where(Theme.id == UUID(theme_id))
            result = await self.db.execute(stmt)
            theme = result.scalar_one_or_none()
            
            if not theme:
                raise ThemeNotFoundError(f"Theme {theme_id} not found")
            
            # Cache result
            await self.redis.setex(
                cache_key,
                self.cache_ttl,
                json.dumps(theme.to_dict(), default=str)
            )
            
            return theme
            
        except ThemeNotFoundError:
            raise
        except Exception as e:
            logger.error(f"Error fetching theme: {str(e)}")
            raise
    
    async def get_presets(self) -> List[Theme]:
        """Get all preset themes."""
        stmt = select(Theme).where(Theme.is_preset == True).order_by(Theme.name)
        result = await self.db.execute(stmt)
        return result.scalars().all()
    
    async def get_business_themes(self, business_id: str) -> List[Theme]:
        """Get all themes for a business."""
        stmt = select(Theme).where(
            and_(
                Theme.business_id == UUID(business_id),
                Theme.is_preset == False
            )
        ).order_by(Theme.updated_at.desc())
        
        result = await self.db.execute(stmt)
        return result.scalars().all()
    
    async def update_theme(self, theme_id: str, update_data: ThemeUpdate, user_id: Optional[str] = None) -> Theme:
        """Update an existing theme."""
        try:
            theme = await self.get_theme(theme_id)
            
            if theme.is_preset:
                raise ThemeValidationError("Cannot modify preset themes")
            
            update_dict = update_data.dict(exclude_unset=True, exclude_none=True)
            
            # Convert nested models to dicts
            for key in ["colors", "typography", "spacing", "effects", "layout"]:
                if key in update_dict and update_dict[key]:
                    update_dict[key] = update_dict[key].dict()
            
            update_dict["updated_at"] = datetime.utcnow()
            
            stmt = (
                update(Theme)
                .where(Theme.id == UUID(theme_id))
                .values(**update_dict)
            )
            
            await self.db.execute(stmt)
            await self.db.commit()
            await self.db.refresh(theme)
            
            # Invalidate cache
            await self._invalidate_cache(theme.id)
            
            return theme
            
        except ThemeValidationError:
            raise
        except Exception as e:
            await self.db.rollback()
            logger.error(f"Failed to update theme: {str(e)}")
            raise
    
    async def delete_theme(self, theme_id: str) -> None:
        """Delete a theme."""
        try:
            theme = await self.get_theme(theme_id)
            
            if theme.is_preset:
                raise ThemeValidationError("Cannot delete preset themes")
            
            if theme.is_active:
                raise ThemeValidationError("Cannot delete active theme. Please activate another theme first.")
            
            stmt = delete(Theme).where(Theme.id == UUID(theme_id))
            await self.db.execute(stmt)
            await self.db.commit()
            
            # Invalidate cache
            await self._invalidate_cache(theme.id)
            
        except ThemeValidationError:
            raise
        except Exception as e:
            await self.db.rollback()
            logger.error(f"Failed to delete theme: {str(e)}")
            raise
    
    async def apply_theme(self, theme_id: str, business_id: str) -> Theme:
        """Apply a theme to a business."""
        try:
            # Deactivate current theme
            stmt = (
                update(Theme)
                .where(
                    and_(
                        Theme.business_id == UUID(business_id),
                        Theme.is_active == True
                    )
                )
                .values(is_active=False)
            )
            await self.db.execute(stmt)
            
            # Activate new theme
            stmt = (
                update(Theme)
                .where(Theme.id == UUID(theme_id))
                .values(is_active=True)
            )
            result = await self.db.execute(stmt)
            
            if result.rowcount == 0:
                raise ThemeNotFoundError(f"Theme {theme_id} not found")
            
            await self.db.commit()
            
            # Fetch and cache updated theme
            theme = await self.get_theme(theme_id)
            
            # Cache as active theme for business
            active_key = f"{self.cache_prefix}active:{business_id}"
            await self.redis.setex(
                active_key,
                self.cache_ttl,
                json.dumps(theme.to_dict(), default=str)
            )
            
            return theme
            
        except ThemeNotFoundError:
            raise
        except Exception as e:
            await self.db.rollback()
            logger.error(f"Failed to apply theme: {str(e)}")
            raise
    
    async def get_active_theme(self, business_id: str) -> Optional[Theme]:
        """Get currently active theme for a business."""
        cache_key = f"{self.cache_prefix}active:{business_id}"
        
        try:
            # Try cache
            cached = await self.redis.get(cache_key)
            if cached:
                data = json.loads(cached)
                return Theme(**data)
            
            # Fetch from DB
            stmt = select(Theme).where(
                and_(
                    Theme.business_id == UUID(business_id),
                    Theme.is_active == True
                )
            )
            result = await self.db.execute(stmt)
            theme = result.scalar_one_or_none()
            
            if theme:
                await self.redis.setex(
                    cache_key,
                    self.cache_ttl,
                    json.dumps(theme.to_dict(), default=str)
                )
            
            return theme
            
        except Exception as e:
            logger.error(f"Error fetching active theme: {str(e)}")
            return None
    
    async def export_theme(self, theme_id: str) -> Dict[str, Any]:
        """Export theme as JSON."""
        theme = await self.get_theme(theme_id)
        
        export_data = {
            "schema_version": "1.0.0",
            "exported_at": datetime.utcnow().isoformat(),
            "theme": theme.to_dict()
        }
        
        return export_data
    
    async def import_theme(self, import_data: Dict[str, Any], business_id: str, user_id: Optional[str] = None) -> Theme:
        """Import theme from JSON."""
        try:
            # Validate schema version
            schema_version = import_data.get("schema_version", "1.0.0")
            if not schema_version.startswith("1."):
                raise ThemeValidationError(f"Unsupported schema version: {schema_version}")
            
            theme_data = import_data.get("theme", {})
            
            # Create new theme from import
            create_data = ThemeCreate(
                name=f"{theme_data.get('name', 'Imported Theme')} (Imported)",
                description=theme_data.get("description"),
                category="imported",
                trade_type=theme_data.get("trade_type"),
                colors=ColorPalette(**theme_data.get("colors", {})),
                typography=Typography(**theme_data.get("typography", {})),
                spacing=Spacing(**theme_data.get("spacing", {})),
                effects=Effects(**theme_data.get("effects", {})),
                layout