# app/modules/tenancy/schemas/store.py """ Pydantic schemas for Store-related operations. Schemas include: - StoreCreate: For creating stores under merchants - StoreUpdate: For updating store information (Admin only) - StoreResponse: Standard store response - StoreDetailResponse: Store response with merchant/owner details - StoreCreateResponse: Response after store creation - StoreListResponse: Paginated store list - StoreSummary: Lightweight store info Note: Ownership transfer is handled at the Merchant level. See models/schema/merchant.py for MerchantTransferOwnership. """ import re from datetime import datetime from pydantic import BaseModel, ConfigDict, Field, field_validator class StoreCreate(BaseModel): """ Schema for creating a new store (storefront/brand) under an existing merchant. Contact info is inherited from the parent merchant by default. Optionally, provide contact fields to override from the start. """ # Parent merchant merchant_id: int = Field(..., description="ID of the parent merchant", gt=0) # Basic Information store_code: str = Field( ..., description="Unique store identifier (e.g., TECHSTORE)", min_length=2, max_length=50, ) subdomain: str = Field( ..., description="Unique subdomain for the store", min_length=2, max_length=100 ) name: str = Field( ..., description="Display name of the store/brand", min_length=2, max_length=255, ) description: str | None = Field(None, description="Store/brand description") # Platform assignments (optional - store can be on multiple platforms) platform_ids: list[int] | None = Field( None, description="List of platform IDs to assign the store to" ) # Marketplace URLs (brand-specific multi-language support) letzshop_csv_url_fr: str | None = Field(None, description="French CSV URL") letzshop_csv_url_en: str | None = Field(None, description="English CSV URL") letzshop_csv_url_de: str | None = Field(None, description="German CSV URL") # Contact Info (optional - if not provided, inherited from merchant) contact_email: str | None = Field( None, description="Override merchant contact email" ) contact_phone: str | None = Field( None, description="Override merchant contact phone" ) website: str | None = Field(None, description="Override merchant website") business_address: str | None = Field( None, description="Override merchant business address" ) tax_number: str | None = Field(None, description="Override merchant tax number") # Language Settings default_language: str | None = Field( "fr", description="Default language for content (en, fr, de, lb)" ) dashboard_language: str | None = Field( "fr", description="Store dashboard UI language" ) storefront_language: str | None = Field( "fr", description="Default storefront language for customers" ) storefront_languages: list[str] | None = Field( default=["fr", "de", "en"], description="Enabled languages for storefront" ) storefront_locale: str | None = Field( None, description="Locale for currency/number formatting (e.g., 'fr-LU', 'de-DE'). NULL = inherit from platform default", max_length=10, ) @field_validator("subdomain") @classmethod def validate_subdomain(cls, v): """Validate subdomain format: lowercase alphanumeric with hyphens.""" if v and not re.match(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$", v): raise ValueError( "Subdomain must contain only lowercase letters, numbers, and hyphens" ) return v.lower() if v else v @field_validator("store_code") @classmethod def validate_store_code(cls, v): """Ensure store code is uppercase for consistency.""" return v.upper() if v else v class StoreUpdate(BaseModel): """ Schema for updating store information (Admin only). Contact fields can be overridden at the store level. Set to null/empty to reset to merchant default (inherit). """ # Basic Information name: str | None = Field(None, min_length=2, max_length=255) description: str | None = None subdomain: str | None = Field(None, min_length=2, max_length=100) # Marketplace URLs (brand-specific) letzshop_csv_url_fr: str | None = None letzshop_csv_url_en: str | None = None letzshop_csv_url_de: str | None = None # Status (Admin only) is_active: bool | None = None is_verified: bool | None = None # Contact Info (set value to override, set to empty string to reset to inherit) contact_email: str | None = Field( None, description="Override merchant contact email" ) contact_phone: str | None = Field( None, description="Override merchant contact phone" ) website: str | None = Field(None, description="Override merchant website") business_address: str | None = Field( None, description="Override merchant business address" ) tax_number: str | None = Field(None, description="Override merchant tax number") # Special flag to reset contact fields to inherit from merchant reset_contact_to_merchant: bool | None = Field( None, description="If true, reset all contact fields to inherit from merchant" ) # Language Settings default_language: str | None = Field( None, description="Default language for content (en, fr, de, lb)" ) dashboard_language: str | None = Field( None, description="Store dashboard UI language" ) storefront_language: str | None = Field( None, description="Default storefront language for customers" ) storefront_languages: list[str] | None = Field( None, description="Enabled languages for storefront" ) storefront_locale: str | None = Field( None, description="Locale for currency/number formatting (e.g., 'fr-LU', 'de-DE'). NULL = inherit from platform default", max_length=10, ) @field_validator("subdomain") @classmethod def subdomain_lowercase(cls, v): """Normalize subdomain to lowercase.""" return v.lower().strip() if v else v model_config = ConfigDict(from_attributes=True) class StoreResponse(BaseModel): """ Standard schema for store response data. Note: Business contact info (contact_email, contact_phone, website, business_address, tax_number) is now at the Merchant level. Use merchant_id to look up merchant details. """ model_config = ConfigDict(from_attributes=True) id: int store_code: str subdomain: str name: str description: str | None # Merchant relationship merchant_id: int # Marketplace URLs (brand-specific) letzshop_csv_url_fr: str | None letzshop_csv_url_en: str | None letzshop_csv_url_de: str | None # Status Flags is_active: bool is_verified: bool # Language Settings (optional with sensible defaults) default_language: str = "fr" dashboard_language: str = "fr" storefront_language: str = "fr" storefront_languages: list[str] = ["fr", "de", "en"] # Currency/number formatting locale (NULL = inherit from platform default) storefront_locale: str | None = None # Timestamps created_at: datetime updated_at: datetime class StoreDetailResponse(StoreResponse): """ Extended store response including merchant information and resolved contact info. Contact fields show the effective value (store override or merchant default) with flags indicating if the value is inherited from the parent merchant. """ # Merchant info merchant_name: str = Field(..., description="Name of the parent merchant") # Owner info (at merchant level) owner_user_id: int = Field(..., description="User ID of the merchant owner") owner_email: str = Field( ..., description="Email of the merchant owner (for login/authentication)" ) owner_username: str = Field(..., description="Username of the merchant owner") # Resolved contact info (store override or merchant default) contact_email: str | None = Field(None, description="Effective contact email") contact_phone: str | None = Field(None, description="Effective contact phone") website: str | None = Field(None, description="Effective website") business_address: str | None = Field(None, description="Effective business address") tax_number: str | None = Field(None, description="Effective tax number") # Inheritance flags (True = value is inherited from merchant, not overridden) contact_email_inherited: bool = Field( False, description="True if contact_email is from merchant" ) contact_phone_inherited: bool = Field( False, description="True if contact_phone is from merchant" ) website_inherited: bool = Field( False, description="True if website is from merchant" ) business_address_inherited: bool = Field( False, description="True if business_address is from merchant" ) tax_number_inherited: bool = Field( False, description="True if tax_number is from merchant" ) # Original merchant values (for reference in UI) merchant_contact_email: str | None = Field( None, description="Merchant's contact email" ) merchant_contact_phone: str | None = Field( None, description="Merchant's phone number" ) merchant_website: str | None = Field(None, description="Merchant's website URL") merchant_business_address: str | None = Field( None, description="Merchant's business address" ) merchant_tax_number: str | None = Field(None, description="Merchant's tax number") class StoreCreateResponse(StoreDetailResponse): """ Response after creating store under an existing merchant. The store is created under a merchant, so no new owner credentials are generated. The merchant owner already has access to this store. """ login_url: str | None = Field(None, description="URL for store storefront") class StoreListResponse(BaseModel): """Schema for paginated store list.""" stores: list[StoreResponse] total: int skip: int limit: int class StoreSummary(BaseModel): """Lightweight store summary for dropdowns and quick references.""" model_config = ConfigDict(from_attributes=True) id: int store_code: str subdomain: str name: str merchant_id: int is_active: bool # NOTE: Store ownership transfer schemas have been removed. # Ownership transfer is now handled at the Merchant level. # See models/schema/merchant.py for MerchantTransferOwnership and MerchantTransferOwnershipResponse. # NOTE: Letzshop export schemas have been moved to app.modules.marketplace.schemas.letzshop # See LetzshopExportRequest, LetzshopExportFileInfo, LetzshopExportResponse # Re-export StoreStatsResponse from core for convenience # This allows tenancy routes to use this schema without importing from core directly from app.modules.core.schemas.dashboard import StoreStatsResponse __all__ = [ "StoreCreate", "StoreUpdate", "StoreResponse", "StoreDetailResponse", "StoreCreateResponse", "StoreListResponse", "StoreSummary", "StoreStatsResponse", ]