refactor: complete module-driven architecture migration
This commit completes the migration to a fully module-driven architecture: ## Models Migration - Moved all domain models from models/database/ to their respective modules: - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc. - cms: MediaFile, VendorTheme - messaging: Email, VendorEmailSettings, VendorEmailTemplate - core: AdminMenuConfig - models/database/ now only contains Base and TimestampMixin (infrastructure) ## Schemas Migration - Moved all domain schemas from models/schema/ to their respective modules: - tenancy: company, vendor, admin, team, vendor_domain - cms: media, image, vendor_theme - messaging: email - models/schema/ now only contains base.py and auth.py (infrastructure) ## Routes Migration - Moved admin routes from app/api/v1/admin/ to modules: - menu_config.py -> core module - modules.py -> tenancy module - module_config.py -> tenancy module - app/api/v1/admin/ now only aggregates auto-discovered module routes ## Menu System - Implemented module-driven menu system with MenuDiscoveryService - Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT - Added MenuItemDefinition and MenuSectionDefinition dataclasses - Each module now defines its own menu items in definition.py - MenuService integrates with MenuDiscoveryService for template rendering ## Documentation - Updated docs/architecture/models-structure.md - Updated docs/architecture/menu-management.md - Updated architecture validation rules for new exceptions ## Architecture Validation - Updated MOD-019 rule to allow base.py in models/schema/ - Created core module exceptions.py and schemas/ directory - All validation errors resolved (only warnings remain) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,23 +1,30 @@
|
||||
# models/schema/__init__.py
|
||||
"""API models package - Pydantic models for request/response validation.
|
||||
"""API models package - Base classes only.
|
||||
|
||||
Note: Many schemas have been migrated to their respective modules:
|
||||
This package provides the base infrastructure for Pydantic schemas:
|
||||
- BaseModel configuration
|
||||
- Common response patterns
|
||||
- Auth schemas (cross-cutting)
|
||||
|
||||
IMPORTANT: Domain schemas have been migrated to their respective modules:
|
||||
- Tenancy schemas: app.modules.tenancy.schemas
|
||||
- CMS schemas: app.modules.cms.schemas
|
||||
- Messaging schemas: app.modules.messaging.schemas
|
||||
- Customer schemas: app.modules.customers.schemas
|
||||
- Order schemas: app.modules.orders.schemas
|
||||
- Inventory schemas: app.modules.inventory.schemas
|
||||
- Message schemas: app.modules.messaging.schemas
|
||||
- Cart schemas: app.modules.cart.schemas
|
||||
- Marketplace schemas: app.modules.marketplace.schemas
|
||||
- Catalog/Product schemas: app.modules.catalog.schemas
|
||||
- Payment schemas: app.modules.payments.schemas
|
||||
|
||||
Import schemas from their canonical module locations instead of this package.
|
||||
"""
|
||||
|
||||
# Import API model modules that remain in legacy location
|
||||
# Infrastructure schemas that remain here
|
||||
from . import (
|
||||
auth,
|
||||
base,
|
||||
email,
|
||||
vendor,
|
||||
)
|
||||
|
||||
# Common imports for convenience
|
||||
@@ -26,6 +33,4 @@ from .base import * # Base Pydantic models
|
||||
__all__ = [
|
||||
"base",
|
||||
"auth",
|
||||
"email",
|
||||
"vendor",
|
||||
]
|
||||
|
||||
@@ -1,590 +0,0 @@
|
||||
# models/schema/admin.py
|
||||
"""
|
||||
Admin-specific Pydantic schemas for API validation and responses.
|
||||
|
||||
This module provides schemas for:
|
||||
- Admin audit logs
|
||||
- Admin notifications
|
||||
- Platform settings
|
||||
- Platform alerts
|
||||
- Bulk operations
|
||||
- System health checks
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
# ============================================================================
|
||||
# ADMIN AUDIT LOG SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class AdminAuditLogResponse(BaseModel):
|
||||
"""Response model for admin audit logs."""
|
||||
|
||||
id: int
|
||||
admin_user_id: int
|
||||
admin_username: str | None = None
|
||||
action: str
|
||||
target_type: str
|
||||
target_id: str
|
||||
details: dict[str, Any] | None = None
|
||||
ip_address: str | None = None
|
||||
user_agent: str | None = None
|
||||
request_id: str | None = None
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class AdminAuditLogFilters(BaseModel):
|
||||
"""Filters for querying audit logs."""
|
||||
|
||||
admin_user_id: int | None = None
|
||||
action: str | None = None
|
||||
target_type: str | None = None
|
||||
date_from: datetime | None = None
|
||||
date_to: datetime | None = None
|
||||
skip: int = Field(0, ge=0)
|
||||
limit: int = Field(100, ge=1, le=1000)
|
||||
|
||||
|
||||
class AdminAuditLogListResponse(BaseModel):
|
||||
"""Paginated list of audit logs."""
|
||||
|
||||
logs: list[AdminAuditLogResponse]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ADMIN NOTIFICATION SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class AdminNotificationCreate(BaseModel):
|
||||
"""Create admin notification."""
|
||||
|
||||
type: str = Field(..., max_length=50, description="Notification type")
|
||||
priority: str = Field(default="normal", description="Priority level")
|
||||
title: str = Field(..., max_length=200)
|
||||
message: str = Field(..., description="Notification message")
|
||||
action_required: bool = Field(default=False)
|
||||
action_url: str | None = Field(None, max_length=500)
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
@field_validator("priority")
|
||||
@classmethod
|
||||
def validate_priority(cls, v):
|
||||
allowed = ["low", "normal", "high", "critical"]
|
||||
if v not in allowed:
|
||||
raise ValueError(f"Priority must be one of: {', '.join(allowed)}")
|
||||
return v
|
||||
|
||||
|
||||
class AdminNotificationResponse(BaseModel):
|
||||
"""Admin notification response."""
|
||||
|
||||
id: int
|
||||
type: str
|
||||
priority: str
|
||||
title: str
|
||||
message: str
|
||||
is_read: bool
|
||||
read_at: datetime | None = None
|
||||
read_by_user_id: int | None = None
|
||||
action_required: bool
|
||||
action_url: str | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class AdminNotificationUpdate(BaseModel):
|
||||
"""Mark notification as read."""
|
||||
|
||||
is_read: bool = True
|
||||
|
||||
|
||||
class AdminNotificationListResponse(BaseModel):
|
||||
"""Paginated list of notifications."""
|
||||
|
||||
notifications: list[AdminNotificationResponse]
|
||||
total: int
|
||||
unread_count: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ADMIN SETTINGS SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class AdminSettingCreate(BaseModel):
|
||||
"""Create or update admin setting."""
|
||||
|
||||
key: str = Field(..., max_length=100, description="Unique setting key")
|
||||
value: str = Field(..., description="Setting value")
|
||||
value_type: str = Field(default="string", description="Data type")
|
||||
category: str | None = Field(None, max_length=50)
|
||||
description: str | None = None
|
||||
is_encrypted: bool = Field(default=False)
|
||||
is_public: bool = Field(default=False, description="Can be exposed to frontend")
|
||||
|
||||
@field_validator("value_type")
|
||||
@classmethod
|
||||
def validate_value_type(cls, v):
|
||||
allowed = ["string", "integer", "boolean", "json", "float"]
|
||||
if v not in allowed:
|
||||
raise ValueError(f"Value type must be one of: {', '.join(allowed)}")
|
||||
return v
|
||||
|
||||
@field_validator("key")
|
||||
@classmethod
|
||||
def validate_key_format(cls, v):
|
||||
# Setting keys should be lowercase with underscores
|
||||
if not v.replace("_", "").isalnum():
|
||||
raise ValueError(
|
||||
"Setting key must contain only letters, numbers, and underscores"
|
||||
)
|
||||
return v.lower()
|
||||
|
||||
|
||||
class AdminSettingResponse(BaseModel):
|
||||
"""Admin setting response."""
|
||||
|
||||
id: int
|
||||
key: str
|
||||
value: str
|
||||
value_type: str
|
||||
category: str | None = None
|
||||
description: str | None = None
|
||||
is_encrypted: bool
|
||||
is_public: bool
|
||||
last_modified_by_user_id: int | None = None
|
||||
updated_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class AdminSettingDefaultResponse(BaseModel):
|
||||
"""Response when returning a default value for non-existent setting."""
|
||||
|
||||
key: str
|
||||
value: str
|
||||
exists: bool = False
|
||||
|
||||
|
||||
class AdminSettingUpdate(BaseModel):
|
||||
"""Update admin setting value."""
|
||||
|
||||
value: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class AdminSettingListResponse(BaseModel):
|
||||
"""List of settings by category."""
|
||||
|
||||
settings: list[AdminSettingResponse]
|
||||
total: int
|
||||
category: str | None = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DISPLAY SETTINGS SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class RowsPerPageResponse(BaseModel):
|
||||
"""Response for rows per page setting."""
|
||||
|
||||
rows_per_page: int
|
||||
|
||||
|
||||
class RowsPerPageUpdateResponse(BaseModel):
|
||||
"""Response after updating rows per page."""
|
||||
|
||||
rows_per_page: int
|
||||
message: str
|
||||
|
||||
|
||||
class PublicDisplaySettingsResponse(BaseModel):
|
||||
"""Public display settings (no auth required)."""
|
||||
|
||||
rows_per_page: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PLATFORM ALERT SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class PlatformAlertCreate(BaseModel):
|
||||
"""Create platform alert."""
|
||||
|
||||
alert_type: str = Field(..., max_length=50)
|
||||
severity: str = Field(..., description="Alert severity")
|
||||
title: str = Field(..., max_length=200)
|
||||
description: str | None = None
|
||||
affected_vendors: list[int] | None = None
|
||||
affected_systems: list[str] | None = None
|
||||
auto_generated: bool = Field(default=True)
|
||||
|
||||
@field_validator("severity")
|
||||
@classmethod
|
||||
def validate_severity(cls, v):
|
||||
allowed = ["info", "warning", "error", "critical"]
|
||||
if v not in allowed:
|
||||
raise ValueError(f"Severity must be one of: {', '.join(allowed)}")
|
||||
return v
|
||||
|
||||
@field_validator("alert_type")
|
||||
@classmethod
|
||||
def validate_alert_type(cls, v):
|
||||
allowed = [
|
||||
"security",
|
||||
"performance",
|
||||
"capacity",
|
||||
"integration",
|
||||
"database",
|
||||
"system",
|
||||
]
|
||||
if v not in allowed:
|
||||
raise ValueError(f"Alert type must be one of: {', '.join(allowed)}")
|
||||
return v
|
||||
|
||||
|
||||
class PlatformAlertResponse(BaseModel):
|
||||
"""Platform alert response."""
|
||||
|
||||
id: int
|
||||
alert_type: str
|
||||
severity: str
|
||||
title: str
|
||||
description: str | None = None
|
||||
affected_vendors: list[int] | None = None
|
||||
affected_systems: list[str] | None = None
|
||||
is_resolved: bool
|
||||
resolved_at: datetime | None = None
|
||||
resolved_by_user_id: int | None = None
|
||||
resolution_notes: str | None = None
|
||||
auto_generated: bool
|
||||
occurrence_count: int
|
||||
first_occurred_at: datetime
|
||||
last_occurred_at: datetime
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class PlatformAlertResolve(BaseModel):
|
||||
"""Resolve platform alert."""
|
||||
|
||||
is_resolved: bool = True
|
||||
resolution_notes: str | None = None
|
||||
|
||||
|
||||
class PlatformAlertListResponse(BaseModel):
|
||||
"""Paginated list of platform alerts."""
|
||||
|
||||
alerts: list[PlatformAlertResponse]
|
||||
total: int
|
||||
active_count: int
|
||||
critical_count: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# BULK OPERATION SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class BulkVendorAction(BaseModel):
|
||||
"""Bulk actions on vendors."""
|
||||
|
||||
vendor_ids: list[int] = Field(..., min_length=1, max_length=100)
|
||||
action: str = Field(..., description="Action to perform")
|
||||
confirm: bool = Field(default=False, description="Required for destructive actions")
|
||||
reason: str | None = Field(None, description="Reason for bulk action")
|
||||
|
||||
@field_validator("action")
|
||||
@classmethod
|
||||
def validate_action(cls, v):
|
||||
allowed = ["activate", "deactivate", "verify", "unverify", "delete"]
|
||||
if v not in allowed:
|
||||
raise ValueError(f"Action must be one of: {', '.join(allowed)}")
|
||||
return v
|
||||
|
||||
|
||||
class BulkVendorActionResponse(BaseModel):
|
||||
"""Response for bulk vendor actions."""
|
||||
|
||||
successful: list[int]
|
||||
failed: dict[int, str] # vendor_id -> error_message
|
||||
total_processed: int
|
||||
action_performed: str
|
||||
message: str
|
||||
|
||||
|
||||
class BulkUserAction(BaseModel):
|
||||
"""Bulk actions on users."""
|
||||
|
||||
user_ids: list[int] = Field(..., min_length=1, max_length=100)
|
||||
action: str = Field(..., description="Action to perform")
|
||||
confirm: bool = Field(default=False)
|
||||
reason: str | None = None
|
||||
|
||||
@field_validator("action")
|
||||
@classmethod
|
||||
def validate_action(cls, v):
|
||||
allowed = ["activate", "deactivate", "delete"]
|
||||
if v not in allowed:
|
||||
raise ValueError(f"Action must be one of: {', '.join(allowed)}")
|
||||
return v
|
||||
|
||||
|
||||
class BulkUserActionResponse(BaseModel):
|
||||
"""Response for bulk user actions."""
|
||||
|
||||
successful: list[int]
|
||||
failed: dict[int, str]
|
||||
total_processed: int
|
||||
action_performed: str
|
||||
message: str
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ADMIN DASHBOARD SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class AdminDashboardStats(BaseModel):
|
||||
"""Comprehensive admin dashboard statistics."""
|
||||
|
||||
platform: dict[str, Any]
|
||||
users: dict[str, Any]
|
||||
vendors: dict[str, Any]
|
||||
products: dict[str, Any]
|
||||
orders: dict[str, Any]
|
||||
imports: dict[str, Any]
|
||||
recent_vendors: list[dict[str, Any]]
|
||||
recent_imports: list[dict[str, Any]]
|
||||
unread_notifications: int
|
||||
active_alerts: int
|
||||
critical_alerts: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SYSTEM HEALTH SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class ComponentHealthStatus(BaseModel):
|
||||
"""Health status for a system component."""
|
||||
|
||||
status: str # healthy, degraded, unhealthy
|
||||
response_time_ms: float | None = None
|
||||
error_message: str | None = None
|
||||
last_checked: datetime
|
||||
details: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class SystemHealthResponse(BaseModel):
|
||||
"""System health check response."""
|
||||
|
||||
overall_status: str # healthy, degraded, critical
|
||||
database: ComponentHealthStatus
|
||||
redis: ComponentHealthStatus
|
||||
celery: ComponentHealthStatus
|
||||
storage: ComponentHealthStatus
|
||||
api_response_time_ms: float
|
||||
uptime_seconds: int
|
||||
timestamp: datetime
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ADMIN SESSION SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class AdminSessionResponse(BaseModel):
|
||||
"""Admin session information."""
|
||||
|
||||
id: int
|
||||
admin_user_id: int
|
||||
admin_username: str | None = None
|
||||
ip_address: str
|
||||
user_agent: str | None = None
|
||||
login_at: datetime
|
||||
last_activity_at: datetime
|
||||
logout_at: datetime | None = None
|
||||
is_active: bool
|
||||
logout_reason: str | None = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class AdminSessionListResponse(BaseModel):
|
||||
"""List of admin sessions."""
|
||||
|
||||
sessions: list[AdminSessionResponse]
|
||||
total: int
|
||||
active_count: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# APPLICATION LOGS SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class ApplicationLogResponse(BaseModel):
|
||||
"""Application log entry response."""
|
||||
|
||||
id: int
|
||||
timestamp: datetime
|
||||
level: str
|
||||
logger_name: str
|
||||
module: str | None = None
|
||||
function_name: str | None = None
|
||||
line_number: int | None = None
|
||||
message: str
|
||||
exception_type: str | None = None
|
||||
exception_message: str | None = None
|
||||
stack_trace: str | None = None
|
||||
request_id: str | None = None
|
||||
user_id: int | None = None
|
||||
vendor_id: int | None = None
|
||||
context: dict[str, Any] | None = None
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class ApplicationLogFilters(BaseModel):
|
||||
"""Filters for querying application logs."""
|
||||
|
||||
level: str | None = Field(None, description="Filter by log level")
|
||||
logger_name: str | None = Field(None, description="Filter by logger name")
|
||||
module: str | None = Field(None, description="Filter by module")
|
||||
user_id: int | None = Field(None, description="Filter by user ID")
|
||||
vendor_id: int | None = Field(None, description="Filter by vendor ID")
|
||||
date_from: datetime | None = Field(None, description="Start date")
|
||||
date_to: datetime | None = Field(None, description="End date")
|
||||
search: str | None = Field(None, description="Search in message")
|
||||
skip: int = Field(0, ge=0)
|
||||
limit: int = Field(100, ge=1, le=1000)
|
||||
|
||||
|
||||
class ApplicationLogListResponse(BaseModel):
|
||||
"""Paginated list of application logs."""
|
||||
|
||||
logs: list[ApplicationLogResponse]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
|
||||
class LogStatistics(BaseModel):
|
||||
"""Statistics about application logs."""
|
||||
|
||||
total_count: int
|
||||
warning_count: int
|
||||
error_count: int
|
||||
critical_count: int
|
||||
by_level: dict[str, int]
|
||||
by_module: dict[str, int]
|
||||
recent_errors: list[ApplicationLogResponse]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# LOG SETTINGS SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class LogSettingsResponse(BaseModel):
|
||||
"""Log configuration settings."""
|
||||
|
||||
log_level: str
|
||||
log_file_max_size_mb: int
|
||||
log_file_backup_count: int
|
||||
db_log_retention_days: int
|
||||
file_logging_enabled: bool
|
||||
db_logging_enabled: bool
|
||||
|
||||
|
||||
class LogSettingsUpdate(BaseModel):
|
||||
"""Update log settings."""
|
||||
|
||||
log_level: str | None = Field(
|
||||
None, description="Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL"
|
||||
)
|
||||
log_file_max_size_mb: int | None = Field(
|
||||
None, ge=1, le=1000, description="Max log file size in MB"
|
||||
)
|
||||
log_file_backup_count: int | None = Field(
|
||||
None, ge=0, le=50, description="Number of backup files to keep"
|
||||
)
|
||||
db_log_retention_days: int | None = Field(
|
||||
None, ge=1, le=365, description="Days to retain logs in database"
|
||||
)
|
||||
|
||||
@field_validator("log_level")
|
||||
@classmethod
|
||||
def validate_log_level(cls, v):
|
||||
if v is not None:
|
||||
allowed = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
||||
if v.upper() not in allowed:
|
||||
raise ValueError(f"Log level must be one of: {', '.join(allowed)}")
|
||||
return v.upper()
|
||||
return v
|
||||
|
||||
|
||||
class FileLogResponse(BaseModel):
|
||||
"""File log content response."""
|
||||
|
||||
filename: str
|
||||
size_bytes: int
|
||||
last_modified: datetime
|
||||
lines: list[str]
|
||||
total_lines: int
|
||||
|
||||
|
||||
class LogFileInfo(BaseModel):
|
||||
"""Log file info for listing."""
|
||||
|
||||
filename: str
|
||||
size_bytes: int
|
||||
last_modified: datetime
|
||||
|
||||
|
||||
class LogFileListResponse(BaseModel):
|
||||
"""Response for listing log files."""
|
||||
|
||||
files: list[LogFileInfo]
|
||||
|
||||
|
||||
class LogDeleteResponse(BaseModel):
|
||||
"""Response for log deletion."""
|
||||
|
||||
message: str
|
||||
|
||||
|
||||
class LogCleanupResponse(BaseModel):
|
||||
"""Response for log cleanup operation."""
|
||||
|
||||
message: str
|
||||
deleted_count: int
|
||||
|
||||
|
||||
class LogSettingsUpdateResponse(BaseModel):
|
||||
"""Response for log settings update."""
|
||||
|
||||
message: str
|
||||
updated_fields: list[str]
|
||||
note: str | None = None
|
||||
@@ -1,216 +0,0 @@
|
||||
# models/schema/company.py
|
||||
"""
|
||||
Pydantic schemas for Company model.
|
||||
|
||||
These schemas are used for API request/response validation and serialization.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
|
||||
|
||||
|
||||
class CompanyBase(BaseModel):
|
||||
"""Base schema for company with common fields."""
|
||||
|
||||
name: str = Field(..., min_length=2, max_length=200, description="Company name")
|
||||
description: str | None = Field(None, description="Company description")
|
||||
contact_email: EmailStr = Field(..., description="Business contact email")
|
||||
contact_phone: str | None = Field(None, description="Business phone number")
|
||||
website: str | None = Field(None, description="Company website URL")
|
||||
business_address: str | None = Field(None, description="Physical business address")
|
||||
tax_number: str | None = Field(None, description="Tax/VAT registration number")
|
||||
|
||||
@field_validator("contact_email")
|
||||
@classmethod
|
||||
def normalize_email(cls, v):
|
||||
"""Normalize email to lowercase."""
|
||||
return v.lower() if v else v
|
||||
|
||||
|
||||
class CompanyCreate(CompanyBase):
|
||||
"""
|
||||
Schema for creating a new company.
|
||||
|
||||
Requires owner_email to create the associated owner user account.
|
||||
"""
|
||||
|
||||
owner_email: EmailStr = Field(
|
||||
..., description="Email for the company owner account"
|
||||
)
|
||||
|
||||
@field_validator("owner_email")
|
||||
@classmethod
|
||||
def normalize_owner_email(cls, v):
|
||||
"""Normalize owner email to lowercase."""
|
||||
return v.lower() if v else v
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class CompanyUpdate(BaseModel):
|
||||
"""
|
||||
Schema for updating company information.
|
||||
|
||||
All fields are optional to support partial updates.
|
||||
"""
|
||||
|
||||
name: str | None = Field(None, min_length=2, max_length=200)
|
||||
description: str | None = None
|
||||
contact_email: EmailStr | None = None
|
||||
contact_phone: str | None = None
|
||||
website: str | None = None
|
||||
business_address: str | None = None
|
||||
tax_number: str | None = None
|
||||
|
||||
# Status (Admin only)
|
||||
is_active: bool | None = None
|
||||
is_verified: bool | None = None
|
||||
|
||||
@field_validator("contact_email")
|
||||
@classmethod
|
||||
def normalize_email(cls, v):
|
||||
"""Normalize email to lowercase."""
|
||||
return v.lower() if v else v
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class CompanyResponse(BaseModel):
|
||||
"""Standard schema for company response data."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
name: str
|
||||
description: str | None
|
||||
|
||||
# Owner information
|
||||
owner_user_id: int
|
||||
|
||||
# Contact Information
|
||||
contact_email: str
|
||||
contact_phone: str | None
|
||||
website: str | None
|
||||
|
||||
# Business Information
|
||||
business_address: str | None
|
||||
tax_number: str | None
|
||||
|
||||
# Status Flags
|
||||
is_active: bool
|
||||
is_verified: bool
|
||||
|
||||
# Timestamps
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
|
||||
class CompanyDetailResponse(CompanyResponse):
|
||||
"""
|
||||
Detailed company response including vendor count and owner details.
|
||||
|
||||
Used for company detail pages and admin views.
|
||||
"""
|
||||
|
||||
# Owner details (from related User)
|
||||
owner_email: str | None = Field(None, description="Owner's email address")
|
||||
owner_username: str | None = Field(None, description="Owner's username")
|
||||
|
||||
# Vendor statistics
|
||||
vendor_count: int = Field(0, description="Number of vendors under this company")
|
||||
active_vendor_count: int = Field(
|
||||
0, description="Number of active vendors under this company"
|
||||
)
|
||||
|
||||
# Vendors list (optional, for detail view)
|
||||
vendors: list | None = Field(None, description="List of vendors under this company")
|
||||
|
||||
|
||||
class CompanyListResponse(BaseModel):
|
||||
"""Schema for paginated company list."""
|
||||
|
||||
companies: list[CompanyResponse]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
|
||||
class CompanyCreateResponse(BaseModel):
|
||||
"""
|
||||
Response after creating a company with owner account.
|
||||
|
||||
Includes temporary password for the owner (shown only once).
|
||||
"""
|
||||
|
||||
company: CompanyResponse
|
||||
owner_user_id: int
|
||||
owner_username: str
|
||||
owner_email: str
|
||||
temporary_password: str = Field(
|
||||
..., description="Temporary password for owner (SHOWN ONLY ONCE)"
|
||||
)
|
||||
login_url: str | None = Field(None, description="URL for company owner to login")
|
||||
|
||||
|
||||
class CompanySummary(BaseModel):
|
||||
"""Lightweight company summary for dropdowns and quick references."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
name: str
|
||||
is_active: bool
|
||||
is_verified: bool
|
||||
vendor_count: int = 0
|
||||
|
||||
|
||||
class CompanyTransferOwnership(BaseModel):
|
||||
"""
|
||||
Schema for transferring company ownership to another user.
|
||||
|
||||
This is a critical operation that requires:
|
||||
- Confirmation flag
|
||||
- Reason for audit trail (optional)
|
||||
"""
|
||||
|
||||
new_owner_user_id: int = Field(
|
||||
..., description="ID of the user who will become the new owner", gt=0
|
||||
)
|
||||
|
||||
confirm_transfer: bool = Field(
|
||||
..., description="Must be true to confirm ownership transfer"
|
||||
)
|
||||
|
||||
transfer_reason: str | None = Field(
|
||||
None,
|
||||
max_length=500,
|
||||
description="Reason for ownership transfer (for audit logs)",
|
||||
)
|
||||
|
||||
@field_validator("confirm_transfer")
|
||||
@classmethod
|
||||
def validate_confirmation(cls, v):
|
||||
"""Ensure confirmation is explicitly true."""
|
||||
if not v:
|
||||
raise ValueError("Ownership transfer requires explicit confirmation")
|
||||
return v
|
||||
|
||||
|
||||
class CompanyTransferOwnershipResponse(BaseModel):
|
||||
"""Response after successful ownership transfer."""
|
||||
|
||||
message: str
|
||||
company_id: int
|
||||
company_name: str
|
||||
|
||||
old_owner: dict[str, Any] = Field(
|
||||
..., description="Information about the previous owner"
|
||||
)
|
||||
new_owner: dict[str, Any] = Field(
|
||||
..., description="Information about the new owner"
|
||||
)
|
||||
|
||||
transferred_at: datetime
|
||||
transfer_reason: str | None
|
||||
@@ -1,247 +0,0 @@
|
||||
# models/schema/email.py
|
||||
"""
|
||||
Email template Pydantic schemas for API responses and requests.
|
||||
|
||||
Provides schemas for:
|
||||
- EmailTemplate: Platform email templates
|
||||
- VendorEmailTemplate: Vendor-specific template overrides
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class EmailTemplateBase(BaseModel):
|
||||
"""Base schema for email templates."""
|
||||
|
||||
code: str = Field(..., description="Template code (e.g., 'password_reset')")
|
||||
language: str = Field(default="en", description="Language code (en, fr, de, lb)")
|
||||
name: str = Field(..., description="Human-readable template name")
|
||||
description: str | None = Field(None, description="Template description")
|
||||
category: str = Field(..., description="Template category (auth, orders, billing, etc.)")
|
||||
subject: str = Field(..., description="Email subject (supports Jinja2 variables)")
|
||||
body_html: str = Field(..., description="HTML email body")
|
||||
body_text: str | None = Field(None, description="Plain text fallback")
|
||||
variables: list[str] = Field(default_factory=list, description="Available variables")
|
||||
|
||||
|
||||
class EmailTemplateCreate(EmailTemplateBase):
|
||||
"""Schema for creating an email template."""
|
||||
|
||||
required_variables: list[str] = Field(
|
||||
default_factory=list, description="Required variables"
|
||||
)
|
||||
is_platform_only: bool = Field(
|
||||
default=False, description="Cannot be overridden by vendors"
|
||||
)
|
||||
|
||||
|
||||
class EmailTemplateUpdate(BaseModel):
|
||||
"""Schema for updating an email template."""
|
||||
|
||||
name: str | None = Field(None, description="Human-readable template name")
|
||||
description: str | None = Field(None, description="Template description")
|
||||
subject: str | None = Field(None, description="Email subject")
|
||||
body_html: str | None = Field(None, description="HTML email body")
|
||||
body_text: str | None = Field(None, description="Plain text fallback")
|
||||
variables: list[str] | None = Field(None, description="Available variables")
|
||||
required_variables: list[str] | None = Field(None, description="Required variables")
|
||||
is_active: bool | None = Field(None, description="Template active status")
|
||||
|
||||
|
||||
class EmailTemplateResponse(BaseModel):
|
||||
"""Schema for email template API response."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
code: str
|
||||
language: str
|
||||
name: str
|
||||
description: str | None
|
||||
category: str
|
||||
subject: str
|
||||
body_html: str
|
||||
body_text: str | None
|
||||
variables: list[str] = Field(default_factory=list)
|
||||
required_variables: list[str] = Field(default_factory=list)
|
||||
is_active: bool
|
||||
is_platform_only: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, template) -> "EmailTemplateResponse":
|
||||
"""Create response from database model."""
|
||||
return cls(
|
||||
id=template.id,
|
||||
code=template.code,
|
||||
language=template.language,
|
||||
name=template.name,
|
||||
description=template.description,
|
||||
category=template.category,
|
||||
subject=template.subject,
|
||||
body_html=template.body_html,
|
||||
body_text=template.body_text,
|
||||
variables=template.variables_list,
|
||||
required_variables=template.required_variables_list,
|
||||
is_active=template.is_active,
|
||||
is_platform_only=template.is_platform_only,
|
||||
created_at=template.created_at,
|
||||
updated_at=template.updated_at,
|
||||
)
|
||||
|
||||
|
||||
class EmailTemplateSummary(BaseModel):
|
||||
"""Summary schema for template list views."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
code: str
|
||||
name: str
|
||||
category: str
|
||||
languages: list[str] = Field(default_factory=list)
|
||||
is_platform_only: bool
|
||||
is_active: bool
|
||||
|
||||
@classmethod
|
||||
def from_db_list(cls, templates: list) -> list["EmailTemplateSummary"]:
|
||||
"""
|
||||
Create summaries from database models, grouping by code.
|
||||
|
||||
Args:
|
||||
templates: List of EmailTemplate models
|
||||
|
||||
Returns:
|
||||
List of EmailTemplateSummary grouped by template code
|
||||
"""
|
||||
# Group templates by code
|
||||
by_code: dict[str, list] = {}
|
||||
for t in templates:
|
||||
if t.code not in by_code:
|
||||
by_code[t.code] = []
|
||||
by_code[t.code].append(t)
|
||||
|
||||
summaries = []
|
||||
for code, group in by_code.items():
|
||||
first = group[0]
|
||||
summaries.append(
|
||||
cls(
|
||||
id=first.id,
|
||||
code=code,
|
||||
name=first.name,
|
||||
category=first.category,
|
||||
languages=[t.language for t in group],
|
||||
is_platform_only=first.is_platform_only,
|
||||
is_active=first.is_active,
|
||||
)
|
||||
)
|
||||
|
||||
return summaries
|
||||
|
||||
|
||||
# Vendor Email Template Schemas
|
||||
|
||||
|
||||
class VendorEmailTemplateCreate(BaseModel):
|
||||
"""Schema for creating a vendor email template override."""
|
||||
|
||||
template_code: str = Field(..., description="Template code to override")
|
||||
language: str = Field(default="en", description="Language code")
|
||||
name: str | None = Field(None, description="Custom name (uses platform default if None)")
|
||||
subject: str = Field(..., description="Custom email subject")
|
||||
body_html: str = Field(..., description="Custom HTML body")
|
||||
body_text: str | None = Field(None, description="Custom plain text body")
|
||||
|
||||
|
||||
class VendorEmailTemplateUpdate(BaseModel):
|
||||
"""Schema for updating a vendor email template override."""
|
||||
|
||||
name: str | None = Field(None, description="Custom name")
|
||||
subject: str | None = Field(None, description="Custom email subject")
|
||||
body_html: str | None = Field(None, description="Custom HTML body")
|
||||
body_text: str | None = Field(None, description="Custom plain text body")
|
||||
is_active: bool | None = Field(None, description="Override active status")
|
||||
|
||||
|
||||
class VendorEmailTemplateResponse(BaseModel):
|
||||
"""Schema for vendor email template override API response."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
vendor_id: int
|
||||
template_code: str
|
||||
language: str
|
||||
name: str | None
|
||||
subject: str
|
||||
body_html: str
|
||||
body_text: str | None
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class EmailTemplateWithOverrideStatus(BaseModel):
|
||||
"""
|
||||
Schema showing template with vendor override status.
|
||||
|
||||
Used in vendor UI to show which templates have been customized.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
code: str
|
||||
name: str
|
||||
category: str
|
||||
languages: list[str]
|
||||
is_platform_only: bool
|
||||
has_override: bool = Field(
|
||||
default=False, description="Whether vendor has customized this template"
|
||||
)
|
||||
override_languages: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Languages with vendor overrides",
|
||||
)
|
||||
|
||||
|
||||
# Email Preview/Test Schemas
|
||||
|
||||
|
||||
class EmailPreviewRequest(BaseModel):
|
||||
"""Schema for requesting an email preview."""
|
||||
|
||||
template_code: str = Field(..., description="Template code")
|
||||
language: str = Field(default="en", description="Language code")
|
||||
variables: dict[str, str] = Field(
|
||||
default_factory=dict, description="Variables to inject"
|
||||
)
|
||||
|
||||
|
||||
class EmailPreviewResponse(BaseModel):
|
||||
"""Schema for email preview response."""
|
||||
|
||||
subject: str
|
||||
body_html: str
|
||||
body_text: str | None
|
||||
|
||||
|
||||
class EmailTestRequest(BaseModel):
|
||||
"""Schema for sending a test email."""
|
||||
|
||||
template_code: str = Field(..., description="Template code")
|
||||
language: str = Field(default="en", description="Language code")
|
||||
to_email: str = Field(..., description="Recipient email address")
|
||||
variables: dict[str, str] = Field(
|
||||
default_factory=dict, description="Variables to inject"
|
||||
)
|
||||
|
||||
|
||||
class EmailTestResponse(BaseModel):
|
||||
"""Schema for test email response."""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
email_log_id: int | None = None
|
||||
@@ -1,46 +0,0 @@
|
||||
# models/schema/image.py
|
||||
"""
|
||||
Pydantic schemas for image operations.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ImageUrls(BaseModel):
|
||||
"""URLs for image variants."""
|
||||
|
||||
original: str
|
||||
medium: str | None = None # 800px variant
|
||||
thumb: str | None = None # 200px variant
|
||||
|
||||
# Allow arbitrary keys for flexibility
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class ImageUploadResponse(BaseModel):
|
||||
"""Response from image upload."""
|
||||
|
||||
success: bool
|
||||
image: dict | None = None
|
||||
error: str | None = None
|
||||
|
||||
|
||||
class ImageDeleteResponse(BaseModel):
|
||||
"""Response from image deletion."""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
|
||||
|
||||
class ImageStorageStats(BaseModel):
|
||||
"""Image storage statistics."""
|
||||
|
||||
total_files: int
|
||||
total_size_bytes: int
|
||||
total_size_mb: float
|
||||
total_size_gb: float
|
||||
directory_count: int
|
||||
max_files_per_dir: int
|
||||
avg_files_per_dir: float
|
||||
products_estimated: int
|
||||
@@ -1,198 +0,0 @@
|
||||
# models/schema/media.py
|
||||
"""
|
||||
Media/file management Pydantic schemas for API validation and responses.
|
||||
|
||||
This module provides schemas for:
|
||||
- Media library listing
|
||||
- File upload responses
|
||||
- Media metadata operations
|
||||
- Media usage tracking
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# ============================================================================
|
||||
# SHARED RESPONSE SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
"""Generic message response for simple operations."""
|
||||
|
||||
message: str
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MEDIA ITEM SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class MediaItemResponse(BaseModel):
|
||||
"""Single media item response."""
|
||||
|
||||
id: int
|
||||
filename: str
|
||||
original_filename: str | None = None
|
||||
file_url: str
|
||||
url: str | None = None # Alias for file_url for JS compatibility
|
||||
thumbnail_url: str | None = None
|
||||
media_type: str # image, video, document
|
||||
mime_type: str | None = None
|
||||
file_size: int | None = None # bytes
|
||||
width: int | None = None # for images/videos
|
||||
height: int | None = None # for images/videos
|
||||
alt_text: str | None = None
|
||||
description: str | None = None
|
||||
folder: str | None = None
|
||||
extra_metadata: dict[str, Any] | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime | None = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
def model_post_init(self, __context: Any) -> None:
|
||||
"""Set url from file_url if not provided."""
|
||||
if self.url is None:
|
||||
object.__setattr__(self, "url", self.file_url)
|
||||
|
||||
|
||||
class MediaListResponse(BaseModel):
|
||||
"""Paginated list of media items."""
|
||||
|
||||
media: list[MediaItemResponse] = []
|
||||
total: int = 0
|
||||
skip: int = 0
|
||||
limit: int = 100
|
||||
message: str | None = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# UPLOAD RESPONSE SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class MediaUploadResponse(BaseModel):
|
||||
"""Response for single file upload."""
|
||||
|
||||
success: bool = True
|
||||
message: str | None = None
|
||||
media: MediaItemResponse | None = None
|
||||
# Legacy fields for backwards compatibility
|
||||
id: int | None = None
|
||||
file_url: str | None = None
|
||||
thumbnail_url: str | None = None
|
||||
filename: str | None = None
|
||||
file_size: int | None = None
|
||||
media_type: str | None = None
|
||||
|
||||
|
||||
class UploadedFileInfo(BaseModel):
|
||||
"""Information about a successfully uploaded file."""
|
||||
|
||||
id: int
|
||||
filename: str
|
||||
file_url: str
|
||||
thumbnail_url: str | None = None
|
||||
|
||||
|
||||
class FailedFileInfo(BaseModel):
|
||||
"""Information about a failed file upload."""
|
||||
|
||||
filename: str
|
||||
error: str
|
||||
|
||||
|
||||
class MultipleUploadResponse(BaseModel):
|
||||
"""Response for multiple file upload."""
|
||||
|
||||
uploaded_files: list[UploadedFileInfo] = []
|
||||
failed_files: list[FailedFileInfo] = []
|
||||
total_uploaded: int = 0
|
||||
total_failed: int = 0
|
||||
message: str | None = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MEDIA DETAIL SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class MediaDetailResponse(BaseModel):
|
||||
"""Detailed media item response with usage info."""
|
||||
|
||||
id: int | None = None
|
||||
filename: str | None = None
|
||||
original_filename: str | None = None
|
||||
file_url: str | None = None
|
||||
thumbnail_url: str | None = None
|
||||
media_type: str | None = None
|
||||
mime_type: str | None = None
|
||||
file_size: int | None = None
|
||||
width: int | None = None
|
||||
height: int | None = None
|
||||
alt_text: str | None = None
|
||||
description: str | None = None
|
||||
folder: str | None = None
|
||||
extra_metadata: dict[str, Any] | None = None
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
message: str | None = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MEDIA UPDATE SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class MediaMetadataUpdate(BaseModel):
|
||||
"""Request model for updating media metadata."""
|
||||
|
||||
filename: str | None = Field(None, max_length=255)
|
||||
alt_text: str | None = Field(None, max_length=500)
|
||||
description: str | None = None
|
||||
folder: str | None = Field(None, max_length=100)
|
||||
metadata: dict[str, Any] | None = None # Named 'metadata' in API, stored as 'extra_metadata'
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MEDIA USAGE SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class ProductUsageInfo(BaseModel):
|
||||
"""Information about product using this media."""
|
||||
|
||||
product_id: int
|
||||
product_name: str
|
||||
usage_type: str # main_image, gallery, variant, etc.
|
||||
|
||||
|
||||
class MediaUsageResponse(BaseModel):
|
||||
"""Response showing where media is being used."""
|
||||
|
||||
media_id: int | None = None
|
||||
products: list[ProductUsageInfo] = []
|
||||
other_usage: list[dict[str, Any]] = []
|
||||
total_usage_count: int = 0
|
||||
message: str | None = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MEDIA OPTIMIZATION SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class OptimizationResultResponse(BaseModel):
|
||||
"""Response for media optimization operation."""
|
||||
|
||||
media_id: int | None = None
|
||||
original_size: int | None = None
|
||||
optimized_size: int | None = None
|
||||
savings_percent: float | None = None
|
||||
optimized_url: str | None = None
|
||||
message: str | None = None
|
||||
@@ -1,293 +0,0 @@
|
||||
# models/schema/team.py
|
||||
"""
|
||||
Pydantic schemas for vendor team management.
|
||||
|
||||
This module defines request/response schemas for:
|
||||
- Team member listing
|
||||
- Team member invitation
|
||||
- Team member updates
|
||||
- Role management
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, EmailStr, Field, field_validator
|
||||
|
||||
# ============================================================================
|
||||
# Role Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class RoleBase(BaseModel):
|
||||
"""Base role schema."""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=100, description="Role name")
|
||||
permissions: list[str] = Field(
|
||||
default_factory=list, description="List of permission strings"
|
||||
)
|
||||
|
||||
|
||||
class RoleCreate(RoleBase):
|
||||
"""Schema for creating a role."""
|
||||
|
||||
|
||||
class RoleUpdate(BaseModel):
|
||||
"""Schema for updating a role."""
|
||||
|
||||
name: str | None = Field(None, min_length=1, max_length=100)
|
||||
permissions: list[str] | None = None
|
||||
|
||||
|
||||
class RoleResponse(RoleBase):
|
||||
"""Schema for role response."""
|
||||
|
||||
id: int
|
||||
vendor_id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True # Pydantic v2 (use orm_mode = True for v1)
|
||||
|
||||
|
||||
class RoleListResponse(BaseModel):
|
||||
"""Schema for role list response."""
|
||||
|
||||
roles: list[RoleResponse]
|
||||
total: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Team Member Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TeamMemberBase(BaseModel):
|
||||
"""Base team member schema."""
|
||||
|
||||
email: EmailStr = Field(..., description="Team member email address")
|
||||
first_name: str | None = Field(None, max_length=100)
|
||||
last_name: str | None = Field(None, max_length=100)
|
||||
|
||||
|
||||
class TeamMemberInvite(TeamMemberBase):
|
||||
"""Schema for inviting a team member."""
|
||||
|
||||
role_id: int | None = Field(
|
||||
None, description="Role ID to assign (for preset roles)"
|
||||
)
|
||||
role_name: str | None = Field(
|
||||
None, description="Role name (manager, staff, support, etc.)"
|
||||
)
|
||||
custom_permissions: list[str] | None = Field(
|
||||
None, description="Custom permissions (overrides role preset)"
|
||||
)
|
||||
|
||||
@field_validator("role_name")
|
||||
def validate_role_name(cls, v):
|
||||
"""Validate role name is in allowed presets."""
|
||||
if v is not None:
|
||||
allowed_roles = ["manager", "staff", "support", "viewer", "marketing"]
|
||||
if v.lower() not in allowed_roles:
|
||||
raise ValueError(
|
||||
f"Role name must be one of: {', '.join(allowed_roles)}"
|
||||
)
|
||||
return v.lower() if v else v
|
||||
|
||||
@field_validator("custom_permissions")
|
||||
def validate_custom_permissions(cls, v, values):
|
||||
"""Ensure either role_id/role_name OR custom_permissions is provided."""
|
||||
if v is not None and len(v) > 0:
|
||||
# If custom permissions provided, role_name should be provided too
|
||||
if "role_name" not in values or not values["role_name"]:
|
||||
raise ValueError(
|
||||
"role_name is required when providing custom_permissions"
|
||||
)
|
||||
return v
|
||||
|
||||
|
||||
class TeamMemberUpdate(BaseModel):
|
||||
"""Schema for updating a team member."""
|
||||
|
||||
role_id: int | None = Field(None, description="New role ID")
|
||||
is_active: bool | None = Field(None, description="Active status")
|
||||
|
||||
|
||||
class TeamMemberResponse(BaseModel):
|
||||
"""Schema for team member response."""
|
||||
|
||||
id: int = Field(..., description="User ID")
|
||||
email: EmailStr
|
||||
username: str
|
||||
first_name: str | None
|
||||
last_name: str | None
|
||||
full_name: str
|
||||
user_type: str = Field(..., description="'owner' or 'member'")
|
||||
role_name: str = Field(..., description="Role name")
|
||||
role_id: int | None
|
||||
permissions: list[str] = Field(
|
||||
default_factory=list, description="User's permissions"
|
||||
)
|
||||
is_active: bool
|
||||
is_owner: bool
|
||||
invitation_pending: bool = Field(
|
||||
default=False, description="True if invitation not yet accepted"
|
||||
)
|
||||
invited_at: datetime | None = Field(None, description="When invitation was sent")
|
||||
accepted_at: datetime | None = Field(
|
||||
None, description="When invitation was accepted"
|
||||
)
|
||||
joined_at: datetime = Field(..., description="When user joined vendor")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TeamMemberListResponse(BaseModel):
|
||||
"""Schema for team member list response."""
|
||||
|
||||
members: list[TeamMemberResponse]
|
||||
total: int
|
||||
active_count: int
|
||||
pending_invitations: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Invitation Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class InvitationAccept(BaseModel):
|
||||
"""Schema for accepting a team invitation."""
|
||||
|
||||
invitation_token: str = Field(
|
||||
..., min_length=32, description="Invitation token from email"
|
||||
)
|
||||
password: str = Field(
|
||||
..., min_length=8, max_length=128, description="Password for new account"
|
||||
)
|
||||
first_name: str = Field(..., min_length=1, max_length=100)
|
||||
last_name: str = Field(..., min_length=1, max_length=100)
|
||||
|
||||
@field_validator("password")
|
||||
def validate_password_strength(cls, v):
|
||||
"""Validate password meets minimum requirements."""
|
||||
if len(v) < 8:
|
||||
raise ValueError("Password must be at least 8 characters long")
|
||||
|
||||
has_upper = any(c.isupper() for c in v)
|
||||
has_lower = any(c.islower() for c in v)
|
||||
has_digit = any(c.isdigit() for c in v)
|
||||
|
||||
if not (has_upper and has_lower and has_digit):
|
||||
raise ValueError(
|
||||
"Password must contain at least one uppercase letter, "
|
||||
"one lowercase letter, and one digit"
|
||||
)
|
||||
|
||||
return v
|
||||
|
||||
|
||||
class InvitationResponse(BaseModel):
|
||||
"""Schema for invitation response."""
|
||||
|
||||
message: str
|
||||
email: EmailStr
|
||||
role: str
|
||||
invitation_token: str | None = Field(
|
||||
None, description="Token (only returned in dev/test environments)"
|
||||
)
|
||||
invitation_sent: bool = Field(default=True)
|
||||
|
||||
|
||||
class InvitationAcceptResponse(BaseModel):
|
||||
"""Schema for invitation acceptance response."""
|
||||
|
||||
message: str
|
||||
vendor: dict = Field(..., description="Vendor information")
|
||||
user: dict = Field(..., description="User information")
|
||||
role: str
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Team Statistics Schema
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TeamStatistics(BaseModel):
|
||||
"""Schema for team statistics."""
|
||||
|
||||
total_members: int
|
||||
active_members: int
|
||||
inactive_members: int
|
||||
pending_invitations: int
|
||||
owners: int
|
||||
team_members: int
|
||||
roles_breakdown: dict = Field(
|
||||
default_factory=dict, description="Count of members per role"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Bulk Operations Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class BulkRemoveRequest(BaseModel):
|
||||
"""Schema for bulk removing team members."""
|
||||
|
||||
user_ids: list[int] = Field(
|
||||
..., min_items=1, description="List of user IDs to remove"
|
||||
)
|
||||
|
||||
|
||||
class BulkRemoveResponse(BaseModel):
|
||||
"""Schema for bulk remove response."""
|
||||
|
||||
success_count: int
|
||||
failed_count: int
|
||||
errors: list[dict] = Field(default_factory=list)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Permission Check Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class PermissionCheckRequest(BaseModel):
|
||||
"""Schema for checking permissions."""
|
||||
|
||||
permissions: list[str] = Field(..., min_items=1, description="Permissions to check")
|
||||
|
||||
|
||||
class PermissionCheckResponse(BaseModel):
|
||||
"""Schema for permission check response."""
|
||||
|
||||
has_all: bool = Field(..., description="True if user has all permissions")
|
||||
has_any: bool = Field(..., description="True if user has any permission")
|
||||
granted: list[str] = Field(default_factory=list, description="Permissions user has")
|
||||
denied: list[str] = Field(
|
||||
default_factory=list, description="Permissions user lacks"
|
||||
)
|
||||
|
||||
|
||||
class UserPermissionsResponse(BaseModel):
|
||||
"""Schema for user's permissions response."""
|
||||
|
||||
permissions: list[str] = Field(default_factory=list)
|
||||
permission_count: int
|
||||
is_owner: bool
|
||||
role_name: str | None = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Error Response Schema
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TeamErrorResponse(BaseModel):
|
||||
"""Schema for team operation errors."""
|
||||
|
||||
error_code: str
|
||||
message: str
|
||||
details: dict | None = None
|
||||
@@ -1,351 +0,0 @@
|
||||
# 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}
|
||||
@@ -1,129 +0,0 @@
|
||||
# models/schema/vendor_domain.py
|
||||
"""
|
||||
Pydantic schemas for Vendor Domain operations.
|
||||
|
||||
Schemas include:
|
||||
- VendorDomainCreate: For adding custom domains
|
||||
- VendorDomainUpdate: For updating domain settings
|
||||
- VendorDomainResponse: Standard domain response
|
||||
- VendorDomainListResponse: Paginated domain list
|
||||
- DomainVerificationInstructions: DNS verification instructions
|
||||
"""
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
class VendorDomainCreate(BaseModel):
|
||||
"""Schema for adding a custom domain to vendor."""
|
||||
|
||||
domain: str = Field(
|
||||
...,
|
||||
description="Custom domain (e.g., myshop.com or shop.mybrand.com)",
|
||||
min_length=3,
|
||||
max_length=255,
|
||||
)
|
||||
is_primary: bool = Field(
|
||||
default=False, description="Set as primary domain for the vendor"
|
||||
)
|
||||
|
||||
@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 VendorDomainUpdate(BaseModel):
|
||||
"""Schema for updating vendor 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 VendorDomainResponse(BaseModel):
|
||||
"""Standard schema for vendor domain response."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
vendor_id: int
|
||||
domain: str
|
||||
is_primary: bool
|
||||
is_active: bool
|
||||
is_verified: bool
|
||||
ssl_status: str
|
||||
verification_token: str | None = None
|
||||
verified_at: datetime | None = None
|
||||
ssl_verified_at: datetime | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class VendorDomainListResponse(BaseModel):
|
||||
"""Schema for paginated vendor domain list."""
|
||||
|
||||
domains: list[VendorDomainResponse]
|
||||
total: int
|
||||
|
||||
|
||||
class DomainVerificationInstructions(BaseModel):
|
||||
"""DNS verification instructions for domain ownership."""
|
||||
|
||||
domain: str
|
||||
verification_token: str
|
||||
instructions: dict[str, str]
|
||||
txt_record: dict[str, str]
|
||||
common_registrars: dict[str, str]
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class DomainVerificationResponse(BaseModel):
|
||||
"""Response after domain verification."""
|
||||
|
||||
message: str
|
||||
domain: str
|
||||
verified_at: datetime
|
||||
is_verified: bool
|
||||
|
||||
|
||||
class DomainDeletionResponse(BaseModel):
|
||||
"""Response after domain deletion."""
|
||||
|
||||
message: str
|
||||
domain: str
|
||||
vendor_id: int
|
||||
@@ -1,108 +0,0 @@
|
||||
# models/schema/vendor_theme.py
|
||||
"""
|
||||
Pydantic schemas for vendor theme operations.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class VendorThemeColors(BaseModel):
|
||||
"""Color scheme for vendor theme."""
|
||||
|
||||
primary: str | None = Field(None, description="Primary brand color")
|
||||
secondary: str | None = Field(None, description="Secondary color")
|
||||
accent: str | None = Field(None, description="Accent/CTA color")
|
||||
background: str | None = Field(None, description="Background color")
|
||||
text: str | None = Field(None, description="Text color")
|
||||
border: str | None = Field(None, description="Border color")
|
||||
|
||||
|
||||
class VendorThemeFonts(BaseModel):
|
||||
"""Typography settings for vendor theme."""
|
||||
|
||||
heading: str | None = Field(None, description="Font for headings")
|
||||
body: str | None = Field(None, description="Font for body text")
|
||||
|
||||
|
||||
class VendorThemeBranding(BaseModel):
|
||||
"""Branding assets for vendor theme."""
|
||||
|
||||
logo: str | None = Field(None, description="Logo URL")
|
||||
logo_dark: str | None = Field(None, description="Dark mode logo URL")
|
||||
favicon: str | None = Field(None, description="Favicon URL")
|
||||
banner: str | None = Field(None, description="Banner image URL")
|
||||
|
||||
|
||||
class VendorThemeLayout(BaseModel):
|
||||
"""Layout settings for vendor theme."""
|
||||
|
||||
style: str | None = Field(
|
||||
None, description="Product layout style (grid, list, masonry)"
|
||||
)
|
||||
header: str | None = Field(
|
||||
None, description="Header style (fixed, static, transparent)"
|
||||
)
|
||||
product_card: str | None = Field(
|
||||
None, description="Product card style (modern, classic, minimal)"
|
||||
)
|
||||
|
||||
|
||||
class VendorThemeUpdate(BaseModel):
|
||||
"""Schema for updating vendor theme (partial updates allowed)."""
|
||||
|
||||
theme_name: str | None = Field(None, description="Theme preset name")
|
||||
colors: dict[str, str] | None = Field(None, description="Color scheme")
|
||||
fonts: dict[str, str] | None = Field(None, description="Font settings")
|
||||
branding: dict[str, str | None] | None = Field(None, description="Branding assets")
|
||||
layout: dict[str, str] | None = Field(None, description="Layout settings")
|
||||
custom_css: str | None = Field(None, description="Custom CSS rules")
|
||||
social_links: dict[str, str] | None = Field(None, description="Social media links")
|
||||
|
||||
|
||||
class VendorThemeResponse(BaseModel):
|
||||
"""Schema for vendor theme response."""
|
||||
|
||||
theme_name: str = Field(..., description="Theme name")
|
||||
colors: dict[str, str] = Field(..., description="Color scheme")
|
||||
fonts: dict[str, str] = Field(..., description="Font settings")
|
||||
branding: dict[str, str | None] = Field(..., description="Branding assets")
|
||||
layout: dict[str, str] = Field(..., description="Layout settings")
|
||||
social_links: dict[str, str] | None = Field(
|
||||
default_factory=dict, description="Social links"
|
||||
)
|
||||
custom_css: str | None = Field(None, description="Custom CSS")
|
||||
css_variables: dict[str, str] | None = Field(
|
||||
None, description="CSS custom properties"
|
||||
)
|
||||
|
||||
|
||||
class ThemePresetPreview(BaseModel):
|
||||
"""Preview information for a theme preset."""
|
||||
|
||||
name: str = Field(..., description="Preset name")
|
||||
description: str = Field(..., description="Preset description")
|
||||
primary_color: str = Field(..., description="Primary color")
|
||||
secondary_color: str = Field(..., description="Secondary color")
|
||||
accent_color: str = Field(..., description="Accent color")
|
||||
heading_font: str = Field(..., description="Heading font")
|
||||
body_font: str = Field(..., description="Body font")
|
||||
layout_style: str = Field(..., description="Layout style")
|
||||
|
||||
|
||||
class ThemePresetResponse(BaseModel):
|
||||
"""Response after applying a preset."""
|
||||
|
||||
message: str = Field(..., description="Success message")
|
||||
theme: VendorThemeResponse = Field(..., description="Applied theme")
|
||||
|
||||
|
||||
class ThemePresetListResponse(BaseModel):
|
||||
"""List of available theme presets."""
|
||||
|
||||
presets: list[ThemePresetPreview] = Field(..., description="Available presets")
|
||||
|
||||
|
||||
class ThemeDeleteResponse(BaseModel):
|
||||
"""Response after deleting a theme."""
|
||||
|
||||
message: str = Field(..., description="Success message")
|
||||
Reference in New Issue
Block a user