- Add database fields for language preferences: - Vendor: dashboard_language, storefront_language, storefront_languages - User: preferred_language - Customer: preferred_language - Add language middleware for request-level language detection: - Cookie-based persistence - Browser Accept-Language fallback - Vendor storefront language constraints - Add language API endpoints (/api/v1/language/*): - POST /set - Set language preference - GET /current - Get current language info - GET /list - List available languages - DELETE /clear - Clear preference - Add i18n utilities (app/utils/i18n.py): - JSON-based translation loading - Jinja2 template integration - Language resolution helpers - Add reusable language selector macros for templates - Add languageSelector() Alpine.js component - Add translation files (en, fr, de, lb) in static/locales/ - Add architecture rules documentation for language implementation - Update marketplace-product-detail.js to use native language names 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
265 lines
9.6 KiB
Python
265 lines
9.6 KiB
Python
# 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.
|