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:
2026-02-01 21:02:56 +01:00
parent 09d7d282c6
commit d7a0ff8818
307 changed files with 5536 additions and 3826 deletions

View File

@@ -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",
]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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}

View File

@@ -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

View File

@@ -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")