User create page: - When role=admin, show super admin toggle - If not super admin, show platform multi-select - Admin users created via /api/v1/admin/admin-users endpoint - Vendor users created via existing /admin/users endpoint Vendor create page: - Add platform selection section - Vendors can be assigned to multiple platforms on creation - Update VendorCreate schema to accept platform_ids - Update AdminService.create_vendor() to create VendorPlatform records Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
352 lines
12 KiB
Python
352 lines
12 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")
|
|
|
|
# Platform assignments (optional - vendor can be on multiple platforms)
|
|
platform_ids: list[int] | None = Field(
|
|
None, description="List of platform IDs to assign the vendor 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 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"
|
|
)
|
|
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("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"
|
|
)
|
|
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 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"]
|
|
|
|
# Currency/number formatting locale (NULL = inherit from platform default)
|
|
storefront_locale: str | None = None
|
|
|
|
# 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]
|
|
celery_task_id: str | None = None # Set when using Celery async export
|
|
is_async: bool = Field(default=False, serialization_alias="async") # True when queued via Celery
|
|
|
|
model_config = {"populate_by_name": True}
|