# app/modules/tenancy/schemas/merchant_domain.py """ Pydantic schemas for Merchant Domain operations. Schemas include: - MerchantDomainCreate: For adding custom domains to merchants - MerchantDomainUpdate: For updating domain settings - MerchantDomainResponse: Standard domain response - MerchantDomainListResponse: Paginated domain list """ import re from datetime import datetime from pydantic import BaseModel, ConfigDict, Field, field_validator class MerchantDomainCreate(BaseModel): """Schema for adding a custom domain to a merchant.""" domain: str = Field( ..., description="Custom domain (e.g., myloyaltyprogram.lu)", min_length=3, max_length=255, ) is_primary: bool = Field( default=True, description="Set as primary domain for the merchant" ) platform_id: int | None = Field(None, description="Platform this domain belongs to") @field_validator("domain") @classmethod def validate_domain(cls, v: str) -> str: """Validate and normalize domain.""" # Remove protocol if present domain = v.replace("https://", "").replace("http://", "") # noqa: SEC-034 # Remove trailing slash domain = domain.rstrip("/") # Convert to lowercase domain = domain.lower().strip() # Basic validation if not domain or "/" in domain: raise ValueError("Invalid domain format") if "." not in domain: raise ValueError("Domain must have at least one dot") # Check for reserved subdomains reserved = ["www", "admin", "api", "mail", "smtp", "ftp", "cpanel", "webmail"] first_part = domain.split(".")[0] if first_part in reserved: raise ValueError( f"Domain cannot start with reserved subdomain: {first_part}" ) # Validate domain format (basic regex) domain_pattern = r"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$" if not re.match(domain_pattern, domain): raise ValueError("Invalid domain format") return domain class MerchantDomainUpdate(BaseModel): """Schema for updating merchant domain settings.""" is_primary: bool | None = Field(None, description="Set as primary domain") is_active: bool | None = Field(None, description="Activate or deactivate domain") model_config = ConfigDict(from_attributes=True) class MerchantDomainResponse(BaseModel): """Standard schema for merchant domain response.""" model_config = ConfigDict(from_attributes=True) id: int merchant_id: int domain: str is_primary: bool is_active: bool is_verified: bool ssl_status: str platform_id: int | None = None verification_token: str | None = None verified_at: datetime | None = None ssl_verified_at: datetime | None = None created_at: datetime updated_at: datetime class MerchantDomainListResponse(BaseModel): """Schema for paginated merchant domain list.""" domains: list[MerchantDomainResponse] total: int class MerchantDomainDeletionResponse(BaseModel): """Response after merchant domain deletion.""" message: str domain: str merchant_id: int