# models/schema/vendor.py """ Pydantic schemas for Vendor-related operations. Schemas include: - VendorCreate: For creating vendors under companies - VendorUpdate: For updating vendor information (Admin only) - VendorResponse: Standard vendor response - VendorDetailResponse: Vendor response with company/owner details - VendorCreateResponse: Response after vendor creation - VendorListResponse: Paginated vendor list - VendorSummary: Lightweight vendor info Note: Ownership transfer is handled at the Company level. See models/schema/company.py for CompanyTransferOwnership. """ import re from datetime import datetime from pydantic import BaseModel, ConfigDict, Field, field_validator class VendorCreate(BaseModel): """ Schema for creating a new vendor (storefront/brand) under an existing company. Contact info is inherited from the parent company by default. Optionally, provide contact fields to override from the start. """ # Parent company company_id: int = Field(..., description="ID of the parent company", gt=0) # Basic Information vendor_code: str = Field( ..., description="Unique vendor identifier (e.g., TECHSTORE)", min_length=2, max_length=50, ) subdomain: str = Field( ..., description="Unique subdomain for the vendor", min_length=2, max_length=100 ) name: str = Field( ..., description="Display name of the vendor/brand", min_length=2, max_length=255, ) description: str | None = Field(None, description="Vendor/brand description") # 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 company) contact_email: str | None = Field( None, description="Override company contact email" ) contact_phone: str | None = Field( None, description="Override company contact phone" ) website: str | None = Field(None, description="Override company website") business_address: str | None = Field( None, description="Override company business address" ) tax_number: str | None = Field(None, description="Override company 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="Vendor 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" ) @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("vendor_code") @classmethod def validate_vendor_code(cls, v): """Ensure vendor code is uppercase for consistency.""" return v.upper() if v else v class VendorUpdate(BaseModel): """ Schema for updating vendor information (Admin only). Contact fields can be overridden at the vendor level. Set to null/empty to reset to company 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 company contact email" ) contact_phone: str | None = Field( None, description="Override company contact phone" ) website: str | None = Field(None, description="Override company website") business_address: str | None = Field( None, description="Override company business address" ) tax_number: str | None = Field(None, description="Override company tax number") # Special flag to reset contact fields to inherit from company reset_contact_to_company: bool | None = Field( None, description="If true, reset all contact fields to inherit from company" ) # Language Settings default_language: str | None = Field( None, description="Default language for content (en, fr, de, lb)" ) dashboard_language: str | None = Field( None, description="Vendor 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" ) @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 VendorResponse(BaseModel): """ Standard schema for vendor response data. Note: Business contact info (contact_email, contact_phone, website, business_address, tax_number) is now at the Company level. Use company_id to look up company details. """ model_config = ConfigDict(from_attributes=True) id: int vendor_code: str subdomain: str name: str description: str | None # Company relationship company_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 defaults for backward compatibility) default_language: str = "fr" dashboard_language: str = "fr" storefront_language: str = "fr" storefront_languages: list[str] = ["fr", "de", "en"] # Timestamps created_at: datetime updated_at: datetime class VendorDetailResponse(VendorResponse): """ Extended vendor response including company information and resolved contact info. Contact fields show the effective value (vendor override or company default) with flags indicating if the value is inherited from the parent company. """ # Company info company_name: str = Field(..., description="Name of the parent company") # Owner info (at company level) owner_email: str = Field( ..., description="Email of the company owner (for login/authentication)" ) owner_username: str = Field(..., description="Username of the company owner") # Resolved contact info (vendor override or company 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 company, not overridden) contact_email_inherited: bool = Field( False, description="True if contact_email is from company" ) contact_phone_inherited: bool = Field( False, description="True if contact_phone is from company" ) website_inherited: bool = Field( False, description="True if website is from company" ) business_address_inherited: bool = Field( False, description="True if business_address is from company" ) tax_number_inherited: bool = Field( False, description="True if tax_number is from company" ) # Original company values (for reference in UI) company_contact_email: str | None = Field( None, description="Company's contact email" ) company_contact_phone: str | None = Field( None, description="Company's phone number" ) company_website: str | None = Field(None, description="Company's website URL") company_business_address: str | None = Field( None, description="Company's business address" ) company_tax_number: str | None = Field(None, description="Company's tax number") class VendorCreateResponse(VendorDetailResponse): """ Response after creating vendor under an existing company. The vendor is created under a company, so no new owner credentials are generated. The company owner already has access to this vendor. """ login_url: str | None = Field(None, description="URL for vendor storefront") class VendorListResponse(BaseModel): """Schema for paginated vendor list.""" vendors: list[VendorResponse] total: int skip: int limit: int class VendorSummary(BaseModel): """Lightweight vendor summary for dropdowns and quick references.""" model_config = ConfigDict(from_attributes=True) id: int vendor_code: str subdomain: str name: str company_id: int is_active: bool # NOTE: Vendor ownership transfer schemas have been removed. # Ownership transfer is now handled at the Company level. # See models/schema/company.py for CompanyTransferOwnership and CompanyTransferOwnershipResponse. # ============================================================================ # LETZSHOP EXPORT SCHEMAS # ============================================================================ class LetzshopExportRequest(BaseModel): """Request body for Letzshop export to pickup folder.""" include_inactive: bool = Field( default=False, description="Include inactive products in export" ) class LetzshopExportFileInfo(BaseModel): """Info about an exported file.""" language: str filename: str | None = None path: str | None = None size_bytes: int | None = None error: str | None = None class LetzshopExportResponse(BaseModel): """Response from Letzshop export to folder.""" success: bool message: str vendor_code: str export_directory: str files: list[LetzshopExportFileInfo]