style: apply black and isort formatting across entire codebase

- Standardize quote style (single to double quotes)
- Reorder and group imports alphabetically
- Fix line breaks and indentation for consistency
- Apply PEP 8 formatting standards

Also updated Makefile to exclude both venv and .venv from code quality checks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-28 19:30:17 +01:00
parent 13f0094743
commit 21c13ca39b
236 changed files with 8450 additions and 6545 deletions

View File

@@ -1,17 +1,16 @@
# models/__init__.py
"""Models package - Database and API models."""
# Database models (SQLAlchemy)
from .database.base import Base
from .database.user import User
from .database.marketplace_product import MarketplaceProduct
from .database.inventory import Inventory
from .database.vendor import Vendor
from .database.product import Product
from .database.marketplace_import_job import MarketplaceImportJob
# API models (Pydantic) - import the modules, not all classes
from . import schema
# Database models (SQLAlchemy)
from .database.base import Base
from .database.inventory import Inventory
from .database.marketplace_import_job import MarketplaceImportJob
from .database.marketplace_product import MarketplaceProduct
from .database.product import Product
from .database.user import User
from .database.vendor import Vendor
# Export database models for Alembic
__all__ = [

View File

@@ -1,17 +1,18 @@
# models/database/__init__.py
"""Database models package."""
from .admin import AdminAuditLog, AdminNotification, AdminSetting, PlatformAlert, AdminSession
from .admin import (AdminAuditLog, AdminNotification, AdminSession,
AdminSetting, PlatformAlert)
from .base import Base
from .customer import Customer, CustomerAddress
from .order import Order, OrderItem
from .user import User
from .marketplace_product import MarketplaceProduct
from .inventory import Inventory
from .vendor import Vendor, Role, VendorUser
from .marketplace_import_job import MarketplaceImportJob
from .marketplace_product import MarketplaceProduct
from .order import Order, OrderItem
from .product import Product
from .user import User
from .vendor import Role, Vendor, VendorUser
from .vendor_domain import VendorDomain
from .vendor_theme import VendorTheme
from .product import Product
from .marketplace_import_job import MarketplaceImportJob
__all__ = [
# Admin-specific models
@@ -34,5 +35,5 @@ __all__ = [
"MarketplaceImportJob",
"MarketplaceProduct",
"VendorDomain",
"VendorTheme"
"VendorTheme",
]

View File

@@ -1,4 +1,4 @@
# Admin-specific models
# Admin-specific models
# models/database/admin.py
"""
Admin-specific database models.
@@ -10,9 +10,12 @@ This module provides models for:
- Platform alerts (system-wide issues)
"""
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, JSON, ForeignKey
from sqlalchemy import (JSON, Boolean, Column, DateTime, ForeignKey, Integer,
String, Text)
from sqlalchemy.orm import relationship
from app.core.database import Base
from .base import TimestampMixin
@@ -23,12 +26,17 @@ class AdminAuditLog(Base, TimestampMixin):
Separate from regular audit logs - focuses on admin-specific operations
like vendor creation, user management, and system configuration changes.
"""
__tablename__ = "admin_audit_logs"
id = Column(Integer, primary_key=True, index=True)
admin_user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
action = Column(String(100), nullable=False, index=True) # create_vendor, delete_vendor, etc.
target_type = Column(String(50), nullable=False, index=True) # vendor, user, import_job, setting
action = Column(
String(100), nullable=False, index=True
) # create_vendor, delete_vendor, etc.
target_type = Column(
String(50), nullable=False, index=True
) # vendor, user, import_job, setting
target_id = Column(String(100), nullable=False, index=True)
details = Column(JSON) # Additional context about the action
ip_address = Column(String(45)) # IPv4 or IPv6
@@ -49,11 +57,16 @@ class AdminNotification(Base, TimestampMixin):
Different from vendor/customer notifications - these are for platform
administrators to track system health and issues requiring attention.
"""
__tablename__ = "admin_notifications"
id = Column(Integer, primary_key=True, index=True)
type = Column(String(50), nullable=False, index=True) # system_alert, vendor_issue, import_failure
priority = Column(String(20), default="normal", index=True) # low, normal, high, critical
type = Column(
String(50), nullable=False, index=True
) # system_alert, vendor_issue, import_failure
priority = Column(
String(20), default="normal", index=True
) # low, normal, high, critical
title = Column(String(200), nullable=False)
message = Column(Text, nullable=False)
is_read = Column(Boolean, default=False, index=True)
@@ -84,13 +97,16 @@ class AdminSetting(Base, TimestampMixin):
- smtp_settings
- stripe_api_keys (encrypted)
"""
__tablename__ = "admin_settings"
id = Column(Integer, primary_key=True, index=True)
key = Column(String(100), unique=True, nullable=False, index=True)
value = Column(Text, nullable=False)
value_type = Column(String(20), default="string") # string, integer, boolean, json
category = Column(String(50), index=True) # system, security, marketplace, notifications
category = Column(
String(50), index=True
) # system, security, marketplace, notifications
description = Column(Text)
is_encrypted = Column(Boolean, default=False)
is_public = Column(Boolean, default=False) # Can be exposed to frontend?
@@ -110,11 +126,16 @@ class PlatformAlert(Base, TimestampMixin):
Tracks platform issues, performance problems, security incidents,
and other system-level concerns that require admin attention.
"""
__tablename__ = "platform_alerts"
id = Column(Integer, primary_key=True, index=True)
alert_type = Column(String(50), nullable=False, index=True) # security, performance, capacity, integration
severity = Column(String(20), nullable=False, index=True) # info, warning, error, critical
alert_type = Column(
String(50), nullable=False, index=True
) # security, performance, capacity, integration
severity = Column(
String(20), nullable=False, index=True
) # info, warning, error, critical
title = Column(String(200), nullable=False)
description = Column(Text)
affected_vendors = Column(JSON) # List of affected vendor IDs
@@ -142,6 +163,7 @@ class AdminSession(Base, TimestampMixin):
Helps identify suspicious login patterns, track concurrent sessions,
and enforce session policies for admin users.
"""
__tablename__ = "admin_sessions"
id = Column(Integer, primary_key=True, index=True)

View File

@@ -1 +1 @@
# AuditLog, DataExportLog models
# AuditLog, DataExportLog models

View File

@@ -1 +1 @@
# BackupLog, RestoreLog models
# BackupLog, RestoreLog models

View File

@@ -10,5 +10,8 @@ class TimestampMixin:
created_at = Column(DateTime, default=datetime.now(timezone.utc), nullable=False)
updated_at = Column(
DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc), nullable=False
DateTime,
default=datetime.now(timezone.utc),
onupdate=datetime.now(timezone.utc),
nullable=False,
)

View File

@@ -1,7 +1,9 @@
# models/database/cart.py
"""Cart item database model."""
from datetime import datetime
from sqlalchemy import Column, Float, ForeignKey, Index, Integer, String, UniqueConstraint
from sqlalchemy import (Column, Float, ForeignKey, Index, Integer, String,
UniqueConstraint)
from sqlalchemy.orm import relationship
from app.core.database import Base
@@ -15,6 +17,7 @@ class CartItem(Base, TimestampMixin):
Stores cart items per session, vendor, and product.
Sessions are identified by a session_id string (from browser cookies).
"""
__tablename__ = "cart_items"
id = Column(Integer, primary_key=True, index=True)

View File

@@ -1 +1 @@
# PlatformConfig, VendorConfig, FeatureFlag models
# PlatformConfig, VendorConfig, FeatureFlag models

View File

@@ -15,7 +15,9 @@ Features:
"""
from datetime import datetime, timezone
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, Index
from sqlalchemy import (Boolean, Column, DateTime, ForeignKey, Index, Integer,
String, Text, UniqueConstraint)
from sqlalchemy.orm import relationship
from app.core.database import Base
@@ -34,15 +36,20 @@ class ContentPage(Base):
2. If not found, use platform default (slug only)
3. If neither exists, show 404 or default template
"""
__tablename__ = "content_pages"
id = Column(Integer, primary_key=True, index=True)
# Vendor association (NULL = platform default)
vendor_id = Column(Integer, ForeignKey("vendors.id", ondelete="CASCADE"), nullable=True, index=True)
vendor_id = Column(
Integer, ForeignKey("vendors.id", ondelete="CASCADE"), nullable=True, index=True
)
# Page identification
slug = Column(String(100), nullable=False, index=True) # about, faq, contact, shipping, returns, etc.
slug = Column(
String(100), nullable=False, index=True
) # about, faq, contact, shipping, returns, etc.
title = Column(String(200), nullable=False)
# Content
@@ -68,12 +75,25 @@ class ContentPage(Base):
show_in_header = Column(Boolean, default=False)
# Timestamps
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False)
created_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
nullable=False,
)
updated_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
nullable=False,
)
# Author tracking (admin or vendor user who created/updated)
created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
updated_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
created_by = Column(
Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
updated_by = Column(
Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
# Relationships
vendor = relationship("Vendor", back_populates="content_pages")
@@ -84,11 +104,10 @@ class ContentPage(Base):
__table_args__ = (
# Unique combination: vendor can only have one page per slug
# Platform defaults (vendor_id=NULL) can only have one page per slug
UniqueConstraint('vendor_id', 'slug', name='uq_vendor_slug'),
UniqueConstraint("vendor_id", "slug", name="uq_vendor_slug"),
# Indexes for performance
Index('idx_vendor_published', 'vendor_id', 'is_published'),
Index('idx_slug_published', 'slug', 'is_published'),
Index("idx_vendor_published", "vendor_id", "is_published"),
Index("idx_slug_published", "slug", "is_published"),
)
def __repr__(self):
@@ -119,7 +138,9 @@ class ContentPage(Base):
"meta_description": self.meta_description,
"meta_keywords": self.meta_keywords,
"is_published": self.is_published,
"published_at": self.published_at.isoformat() if self.published_at else None,
"published_at": (
self.published_at.isoformat() if self.published_at else None
),
"display_order": self.display_order,
"show_in_footer": self.show_in_footer,
"show_in_header": self.show_in_header,

View File

@@ -1,8 +1,12 @@
from datetime import datetime
from decimal import Decimal
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, JSON, Numeric
from sqlalchemy import (JSON, Boolean, Column, DateTime, ForeignKey, Integer,
Numeric, String, Text)
from sqlalchemy.orm import relationship
from app.core.database import Base
from .base import TimestampMixin
@@ -11,12 +15,16 @@ class Customer(Base, TimestampMixin):
id = Column(Integer, primary_key=True, index=True)
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
email = Column(String(255), nullable=False, index=True) # Unique within vendor scope
email = Column(
String(255), nullable=False, index=True
) # Unique within vendor scope
hashed_password = Column(String(255), nullable=False)
first_name = Column(String(100))
last_name = Column(String(100))
phone = Column(String(50))
customer_number = Column(String(100), nullable=False, index=True) # Vendor-specific ID
customer_number = Column(
String(100), nullable=False, index=True
) # Vendor-specific ID
preferences = Column(JSON, default=dict)
marketing_consent = Column(Boolean, default=False)
last_order_date = Column(DateTime)

View File

@@ -1,6 +1,8 @@
# models/database/inventory.py
from datetime import datetime
from sqlalchemy import Column, ForeignKey, Index, Integer, String, UniqueConstraint
from sqlalchemy import (Column, ForeignKey, Index, Integer, String,
UniqueConstraint)
from sqlalchemy.orm import relationship
from app.core.database import Base
@@ -27,7 +29,9 @@ class Inventory(Base, TimestampMixin):
# Constraints
__table_args__ = (
UniqueConstraint("product_id", "location", name="uq_inventory_product_location"),
UniqueConstraint(
"product_id", "location", name="uq_inventory_product_location"
),
Index("idx_inventory_vendor_product", "vendor_id", "product_id"),
Index("idx_inventory_product_location", "product_id", "location"),
)

View File

@@ -1 +1 @@
# MarketplaceImportJob model
# MarketplaceImportJob model

View File

@@ -1,6 +1,7 @@
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, ForeignKey, Index, Integer, String, Text
from sqlalchemy import (Column, DateTime, ForeignKey, Index, Integer, String,
Text)
from sqlalchemy.orm import relationship
from app.core.database import Base

View File

@@ -55,7 +55,9 @@ class MarketplaceProduct(Base, TimestampMixin):
marketplace = Column(
String, index=True, nullable=True, default="Letzshop"
) # Index for marketplace filtering
vendor_name = Column(String, index=True, nullable=True) # Index for vendor filtering
vendor_name = Column(
String, index=True, nullable=True
) # Index for vendor filtering
product = relationship("Product", back_populates="marketplace_product")

View File

@@ -1 +1 @@
# MediaFile, ProductMedia models
# MediaFile, ProductMedia models

View File

@@ -1 +1 @@
# PerformanceMetric, ErrorLog, SystemAlert models
# PerformanceMetric, ErrorLog, SystemAlert models

View File

@@ -1 +1 @@
# NotificationTemplate, NotificationQueue, NotificationLog models
# NotificationTemplate, NotificationQueue, NotificationLog models

View File

@@ -1,6 +1,8 @@
# models/database/order.py
from datetime import datetime
from sqlalchemy import Column, DateTime, Float, ForeignKey, Integer, String, Text, Boolean
from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer,
String, Text)
from sqlalchemy.orm import relationship
from app.core.database import Base
@@ -9,11 +11,14 @@ from models.database.base import TimestampMixin
class Order(Base, TimestampMixin):
"""Customer orders."""
__tablename__ = "orders"
id = Column(Integer, primary_key=True, index=True)
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False, index=True)
customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False, index=True)
customer_id = Column(
Integer, ForeignKey("customers.id"), nullable=False, index=True
)
order_number = Column(String, nullable=False, unique=True, index=True)
@@ -30,8 +35,12 @@ class Order(Base, TimestampMixin):
currency = Column(String, default="EUR")
# Addresses (stored as IDs)
shipping_address_id = Column(Integer, ForeignKey("customer_addresses.id"), nullable=False)
billing_address_id = Column(Integer, ForeignKey("customer_addresses.id"), nullable=False)
shipping_address_id = Column(
Integer, ForeignKey("customer_addresses.id"), nullable=False
)
billing_address_id = Column(
Integer, ForeignKey("customer_addresses.id"), nullable=False
)
# Shipping
shipping_method = Column(String, nullable=True)
@@ -50,8 +59,12 @@ class Order(Base, TimestampMixin):
# Relationships
vendor = relationship("Vendor")
customer = relationship("Customer", back_populates="orders")
items = relationship("OrderItem", back_populates="order", cascade="all, delete-orphan")
shipping_address = relationship("CustomerAddress", foreign_keys=[shipping_address_id])
items = relationship(
"OrderItem", back_populates="order", cascade="all, delete-orphan"
)
shipping_address = relationship(
"CustomerAddress", foreign_keys=[shipping_address_id]
)
billing_address = relationship("CustomerAddress", foreign_keys=[billing_address_id])
def __repr__(self):
@@ -60,6 +73,7 @@ class Order(Base, TimestampMixin):
class OrderItem(Base, TimestampMixin):
"""Individual items in an order."""
__tablename__ = "order_items"
id = Column(Integer, primary_key=True, index=True)

View File

@@ -1 +1 @@
# Payment, PaymentMethod, VendorPaymentConfig models
# Payment, PaymentMethod, VendorPaymentConfig models

View File

@@ -1,6 +1,8 @@
# models/database/product.py
from datetime import datetime
from sqlalchemy import Boolean, Column, Float, ForeignKey, Index, Integer, String, UniqueConstraint
from sqlalchemy import (Boolean, Column, Float, ForeignKey, Index, Integer,
String, UniqueConstraint)
from sqlalchemy.orm import relationship
from app.core.database import Base
@@ -12,7 +14,9 @@ class Product(Base, TimestampMixin):
id = Column(Integer, primary_key=True, index=True)
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
marketplace_product_id = Column(Integer, ForeignKey("marketplace_products.id"), nullable=False)
marketplace_product_id = Column(
Integer, ForeignKey("marketplace_products.id"), nullable=False
)
# Vendor-specific overrides
product_id = Column(String) # Vendor's internal SKU
@@ -34,7 +38,9 @@ class Product(Base, TimestampMixin):
# Relationships
vendor = relationship("Vendor", back_populates="products")
marketplace_product = relationship("MarketplaceProduct", back_populates="product")
inventory_entries = relationship("Inventory", back_populates="product", cascade="all, delete-orphan")
inventory_entries = relationship(
"Inventory", back_populates="product", cascade="all, delete-orphan"
)
# Constraints
__table_args__ = (

View File

@@ -1 +1 @@
# SearchIndex, SearchQuery models
# SearchIndex, SearchQuery models

View File

@@ -1 +1 @@
# TaskLog model
# TaskLog model

View File

@@ -10,16 +10,18 @@ ROLE CLARIFICATION:
- Vendor-specific roles (manager, staff, etc.) are stored in VendorUser.role
- Customers are NOT in the User table - they use the Customer model
"""
from sqlalchemy import Boolean, Column, DateTime, Integer, String, Enum
from sqlalchemy.orm import relationship
import enum
from sqlalchemy import Boolean, Column, DateTime, Enum, Integer, String
from sqlalchemy.orm import relationship
from app.core.database import Base
from models.database.base import TimestampMixin
class UserRole(str, enum.Enum):
"""Platform-level user roles."""
ADMIN = "admin" # Platform administrator
VENDOR = "vendor" # Vendor owner or team member
@@ -44,12 +46,12 @@ class User(Base, TimestampMixin):
last_login = Column(DateTime, nullable=True)
# Relationships
marketplace_import_jobs = relationship("MarketplaceImportJob", back_populates="user")
marketplace_import_jobs = relationship(
"MarketplaceImportJob", back_populates="user"
)
owned_vendors = relationship("Vendor", back_populates="owner")
vendor_memberships = relationship(
"VendorUser",
foreign_keys="[VendorUser.user_id]",
back_populates="user"
"VendorUser", foreign_keys="[VendorUser.user_id]", back_populates="user"
)
def __repr__(self):
@@ -84,8 +86,7 @@ class User(Base, TimestampMixin):
return True
# Check if team member
return any(
vm.vendor_id == vendor_id and vm.is_active
for vm in self.vendor_memberships
vm.vendor_id == vendor_id and vm.is_active for vm in self.vendor_memberships
)
def get_vendor_role(self, vendor_id: int) -> str:

View File

@@ -5,29 +5,37 @@ Vendor model representing entities that sell products or services.
This module defines the Vendor model along with its relationships to
other models such as User (owner), Product, Customer, Order, and MarketplaceImportJob.
"""
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Text, JSON, DateTime
import enum
from sqlalchemy import (JSON, Boolean, Column, DateTime, ForeignKey, Integer,
String, Text)
from sqlalchemy.orm import relationship
from app.core.config import settings
# Import Base from the central database module instead of creating a new one
from app.core.database import Base
from models.database.base import TimestampMixin
from app.core.config import settings
import enum
class Vendor(Base, TimestampMixin):
"""Represents a vendor in the system."""
__tablename__ = "vendors" # Name of the table in the database
id = Column(Integer, primary_key=True, index=True) # Primary key and indexed column for vendor ID
vendor_code = Column(String, unique=True, index=True,
nullable=False) # Unique, indexed, non-nullable vendor code column
subdomain = Column(String(100), unique=True, nullable=False,
index=True) # Unique, non-nullable subdomain column with indexing
id = Column(
Integer, primary_key=True, index=True
) # Primary key and indexed column for vendor ID
vendor_code = Column(
String, unique=True, index=True, nullable=False
) # Unique, indexed, non-nullable vendor code column
subdomain = Column(
String(100), unique=True, nullable=False, index=True
) # Unique, non-nullable subdomain column with indexing
name = Column(String, nullable=False) # Non-nullable name column for the vendor
description = Column(Text) # Optional text description column for the vendor
owner_user_id = Column(Integer, ForeignKey("users.id"),
nullable=False) # Foreign key to user ID of the vendor's owner
owner_user_id = Column(
Integer, ForeignKey("users.id"), nullable=False
) # Foreign key to user ID of the vendor's owner
# Contact information
contact_email = Column(String) # Optional email column for contact information
@@ -40,34 +48,46 @@ class Vendor(Base, TimestampMixin):
letzshop_csv_url_de = Column(String) # URL for German CSV in Letzshop
# Business information
business_address = Column(Text) # Optional text address column for business information
business_address = Column(
Text
) # Optional text address column for business information
tax_number = Column(String) # Optional tax number column for business information
# Status
is_active = Column(Boolean, default=True) # Boolean to indicate if the vendor is active
is_verified = Column(Boolean, default=False) # Boolean to indicate if the vendor is verified
is_active = Column(
Boolean, default=True
) # Boolean to indicate if the vendor is active
is_verified = Column(
Boolean, default=False
) # Boolean to indicate if the vendor is verified
# ========================================================================
# Relationships
# ========================================================================
owner = relationship("User", back_populates="owned_vendors") # Relationship with User model for the vendor's owner
vendor_users = relationship("VendorUser",
back_populates="vendor") # Relationship with VendorUser model for users in this vendor
products = relationship("Product",
back_populates="vendor") # Relationship with Product model for products of this vendor
customers = relationship("Customer",
back_populates="vendor") # Relationship with Customer model for customers of this vendor
orders = relationship("Order",
back_populates="vendor") # Relationship with Order model for orders placed by this vendor
marketplace_import_jobs = relationship("MarketplaceImportJob",
back_populates="vendor") # Relationship with MarketplaceImportJob model for import jobs related to this vendor
owner = relationship(
"User", back_populates="owned_vendors"
) # Relationship with User model for the vendor's owner
vendor_users = relationship(
"VendorUser", back_populates="vendor"
) # Relationship with VendorUser model for users in this vendor
products = relationship(
"Product", back_populates="vendor"
) # Relationship with Product model for products of this vendor
customers = relationship(
"Customer", back_populates="vendor"
) # Relationship with Customer model for customers of this vendor
orders = relationship(
"Order", back_populates="vendor"
) # Relationship with Order model for orders placed by this vendor
marketplace_import_jobs = relationship(
"MarketplaceImportJob", back_populates="vendor"
) # Relationship with MarketplaceImportJob model for import jobs related to this vendor
domains = relationship(
"VendorDomain",
back_populates="vendor",
cascade="all, delete-orphan",
order_by="VendorDomain.is_primary.desc()"
order_by="VendorDomain.is_primary.desc()",
) # Relationship with VendorDomain model for custom domains of the vendor
# Single theme relationship (ONE vendor = ONE theme)
@@ -77,14 +97,12 @@ class Vendor(Base, TimestampMixin):
"VendorTheme",
back_populates="vendor",
uselist=False,
cascade="all, delete-orphan"
cascade="all, delete-orphan",
) # Relationship with VendorTheme model for the active theme of the vendor
# Content pages relationship (vendor can override platform default pages)
content_pages = relationship(
"ContentPage",
back_populates="vendor",
cascade="all, delete-orphan"
"ContentPage", back_populates="vendor", cascade="all, delete-orphan"
) # Relationship with ContentPage model for vendor-specific content pages
def __repr__(self):
@@ -121,23 +139,16 @@ class Vendor(Base, TimestampMixin):
"accent": "#ec4899",
"background": "#ffffff",
"text": "#1f2937",
"border": "#e5e7eb"
},
"fonts": {
"heading": "Inter, sans-serif",
"body": "Inter, sans-serif"
"border": "#e5e7eb",
},
"fonts": {"heading": "Inter, sans-serif", "body": "Inter, sans-serif"},
"branding": {
"logo": None,
"logo_dark": None,
"favicon": None,
"banner": None
},
"layout": {
"style": "grid",
"header": "fixed",
"product_card": "modern"
"banner": None,
},
"layout": {"style": "grid", "header": "fixed", "product_card": "modern"},
"social_links": {},
"custom_css": None,
"css_variables": {
@@ -149,18 +160,22 @@ class Vendor(Base, TimestampMixin):
"--color-border": "#e5e7eb",
"--font-heading": "Inter, sans-serif",
"--font-body": "Inter, sans-serif",
}
},
}
def get_primary_color(self) -> str:
"""Get primary color from active theme."""
theme = self.get_effective_theme()
return theme.get("colors", {}).get("primary", "#6366f1") # Default to default theme if not found
return theme.get("colors", {}).get(
"primary", "#6366f1"
) # Default to default theme if not found
def get_logo_url(self) -> str:
"""Get logo URL from active theme."""
theme = self.get_effective_theme()
return theme.get("branding", {}).get("logo") # Return None or the logo URL if found
return theme.get("branding", {}).get(
"logo"
) # Return None or the logo URL if found
# ========================================================================
# Domain Helper Methods
@@ -177,7 +192,9 @@ class Vendor(Base, TimestampMixin):
@property
def all_domains(self):
"""Get all active domains (subdomain + custom domains)."""
domains = [f"{self.subdomain}.{settings.platform_domain}"] # Start with the main subdomain
domains = [
f"{self.subdomain}.{settings.platform_domain}"
] # Start with the main subdomain
for domain in self.domains:
if domain.is_active:
domains.append(domain.domain) # Add other active custom domains
@@ -186,6 +203,7 @@ class Vendor(Base, TimestampMixin):
class VendorUserType(str, enum.Enum):
"""Types of vendor users."""
OWNER = "owner" # Vendor owner (full access to vendor area)
TEAM_MEMBER = "member" # Team member (role-based access to vendor area)
@@ -222,14 +240,18 @@ class VendorUser(Base, TimestampMixin):
invitation_sent_at = Column(DateTime, nullable=True)
invitation_accepted_at = Column(DateTime, nullable=True)
is_active = Column(Boolean, default=False, nullable=False) # False until invitation accepted
is_active = Column(
Boolean, default=False, nullable=False
) # False until invitation accepted
"""Indicates whether the VendorUser role is active."""
# Relationships
vendor = relationship("Vendor", back_populates="vendor_users")
"""Relationship to the Vendor model, representing the associated vendor."""
user = relationship("User", foreign_keys=[user_id], back_populates="vendor_memberships")
user = relationship(
"User", foreign_keys=[user_id], back_populates="vendor_memberships"
)
"""Relationship to the User model, representing the user who holds this role within the vendor."""
inviter = relationship("User", foreign_keys=[invited_by])
@@ -287,6 +309,7 @@ class VendorUser(Base, TimestampMixin):
if self.is_owner:
# Return all possible permissions
from app.core.permissions import VendorPermissions
return list(VendorPermissions.__members__.values())
if self.role and self.role.permissions:
@@ -294,6 +317,7 @@ class VendorUser(Base, TimestampMixin):
return []
class Role(Base, TimestampMixin):
"""Represents a role within a vendor's system."""

View File

@@ -3,11 +3,11 @@
Vendor Domain Model - Maps custom domains to vendors
"""
from datetime import datetime, timezone
from sqlalchemy import (
Column, Integer, String, Boolean, DateTime,
ForeignKey, UniqueConstraint, Index
)
from sqlalchemy import (Boolean, Column, DateTime, ForeignKey, Index, Integer,
String, UniqueConstraint)
from sqlalchemy.orm import relationship
from app.core.database import Base
from models.database.base import TimestampMixin
@@ -21,10 +21,13 @@ class VendorDomain(Base, TimestampMixin):
- shop.mybusiness.com → Vendor 2
- www.customdomain1.com → Vendor 1 (www is stripped)
"""
__tablename__ = "vendor_domains"
id = Column(Integer, primary_key=True, index=True)
vendor_id = Column(Integer, ForeignKey("vendors.id", ondelete="CASCADE"), nullable=False)
vendor_id = Column(
Integer, ForeignKey("vendors.id", ondelete="CASCADE"), nullable=False
)
# Domain configuration
domain = Column(String(255), nullable=False, unique=True, index=True)
@@ -32,7 +35,9 @@ class VendorDomain(Base, TimestampMixin):
is_active = Column(Boolean, default=True, nullable=False)
# SSL/TLS status (for monitoring)
ssl_status = Column(String(50), default="pending") # pending, active, expired, error
ssl_status = Column(
String(50), default="pending"
) # pending, active, expired, error
ssl_verified_at = Column(DateTime(timezone=True), nullable=True)
# DNS verification (to confirm domain ownership)
@@ -45,9 +50,9 @@ class VendorDomain(Base, TimestampMixin):
# Constraints
__table_args__ = (
UniqueConstraint('vendor_id', 'domain', name='uq_vendor_domain'),
Index('idx_domain_active', 'domain', 'is_active'),
Index('idx_vendor_primary', 'vendor_id', 'is_primary'),
UniqueConstraint("vendor_id", "domain", name="uq_vendor_domain"),
Index("idx_domain_active", "domain", "is_active"),
Index("idx_vendor_primary", "vendor_id", "is_primary"),
)
def __repr__(self):

View File

@@ -3,8 +3,9 @@
Vendor Theme Configuration Model
Allows each vendor to customize their shop's appearance
"""
from sqlalchemy import Column, Integer, String, Boolean, Text, JSON, ForeignKey
from sqlalchemy import JSON, Boolean, Column, ForeignKey, Integer, String, Text
from sqlalchemy.orm import relationship
from app.core.database import Base
from models.database.base import TimestampMixin
@@ -22,6 +23,7 @@ class VendorTheme(Base, TimestampMixin):
Theme presets available: default, modern, classic, minimal, vibrant
"""
__tablename__ = "vendor_themes"
id = Column(Integer, primary_key=True, index=True)
@@ -29,22 +31,27 @@ class VendorTheme(Base, TimestampMixin):
Integer,
ForeignKey("vendors.id", ondelete="CASCADE"),
nullable=False,
unique=True # ONE vendor = ONE theme
unique=True, # ONE vendor = ONE theme
)
# Basic Theme Settings
theme_name = Column(String(100), default="default") # default, modern, classic, minimal, vibrant
theme_name = Column(
String(100), default="default"
) # default, modern, classic, minimal, vibrant
is_active = Column(Boolean, default=True)
# Color Scheme (JSON for flexibility)
colors = Column(JSON, default={
"primary": "#6366f1", # Indigo
"secondary": "#8b5cf6", # Purple
"accent": "#ec4899", # Pink
"background": "#ffffff", # White
"text": "#1f2937", # Gray-800
"border": "#e5e7eb" # Gray-200
})
colors = Column(
JSON,
default={
"primary": "#6366f1", # Indigo
"secondary": "#8b5cf6", # Purple
"accent": "#ec4899", # Pink
"background": "#ffffff", # White
"text": "#1f2937", # Gray-800
"border": "#e5e7eb", # Gray-200
},
)
# Typography
font_family_heading = Column(String(100), default="Inter, sans-serif")
@@ -59,7 +66,9 @@ class VendorTheme(Base, TimestampMixin):
# Layout Preferences
layout_style = Column(String(50), default="grid") # grid, list, masonry
header_style = Column(String(50), default="fixed") # fixed, static, transparent
product_card_style = Column(String(50), default="modern") # modern, classic, minimal
product_card_style = Column(
String(50), default="modern"
) # modern, classic, minimal
# Custom CSS (for advanced customization)
custom_css = Column(Text, nullable=True)
@@ -68,14 +77,18 @@ class VendorTheme(Base, TimestampMixin):
social_links = Column(JSON, default={}) # {facebook: "url", instagram: "url", etc.}
# SEO & Meta
meta_title_template = Column(String(200), nullable=True) # e.g., "{product_name} - {shop_name}"
meta_title_template = Column(
String(200), nullable=True
) # e.g., "{product_name} - {shop_name}"
meta_description = Column(Text, nullable=True)
# Relationships - FIXED: back_populates must match the relationship name in Vendor model
vendor = relationship("Vendor", back_populates="vendor_theme")
def __repr__(self):
return f"<VendorTheme(vendor_id={self.vendor_id}, theme_name='{self.theme_name}')>"
return (
f"<VendorTheme(vendor_id={self.vendor_id}, theme_name='{self.theme_name}')>"
)
@property
def primary_color(self):

View File

@@ -1,14 +1,9 @@
# models/schema/__init__.py
"""API models package - Pydantic models for request/response validation."""
from . import auth
# Import API model modules
from . import base
from . import marketplace_import_job
from . import marketplace_product
from . import stats
from . import inventory
from . import vendor
from . import (auth, base, inventory, marketplace_import_job,
marketplace_product, stats, vendor)
# Common imports for convenience
from .base import * # Base Pydantic models

View File

@@ -12,16 +12,18 @@ This module provides schemas for:
"""
from datetime import datetime
from typing import Optional, List, Dict, Any
from pydantic import BaseModel, Field, field_validator
from typing import Any, Dict, List, Optional
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: Optional[str] = None
@@ -39,6 +41,7 @@ class AdminAuditLogResponse(BaseModel):
class AdminAuditLogFilters(BaseModel):
"""Filters for querying audit logs."""
admin_user_id: Optional[int] = None
action: Optional[str] = None
target_type: Optional[str] = None
@@ -50,6 +53,7 @@ class AdminAuditLogFilters(BaseModel):
class AdminAuditLogListResponse(BaseModel):
"""Paginated list of audit logs."""
logs: List[AdminAuditLogResponse]
total: int
skip: int
@@ -60,8 +64,10 @@ class AdminAuditLogListResponse(BaseModel):
# 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)
@@ -70,10 +76,10 @@ class AdminNotificationCreate(BaseModel):
action_url: Optional[str] = Field(None, max_length=500)
metadata: Optional[Dict[str, Any]] = None
@field_validator('priority')
@field_validator("priority")
@classmethod
def validate_priority(cls, v):
allowed = ['low', 'normal', 'high', 'critical']
allowed = ["low", "normal", "high", "critical"]
if v not in allowed:
raise ValueError(f"Priority must be one of: {', '.join(allowed)}")
return v
@@ -81,6 +87,7 @@ class AdminNotificationCreate(BaseModel):
class AdminNotificationResponse(BaseModel):
"""Admin notification response."""
id: int
type: str
priority: str
@@ -99,11 +106,13 @@ class AdminNotificationResponse(BaseModel):
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
@@ -115,8 +124,10 @@ class AdminNotificationListResponse(BaseModel):
# 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")
@@ -125,25 +136,28 @@ class AdminSettingCreate(BaseModel):
is_encrypted: bool = Field(default=False)
is_public: bool = Field(default=False, description="Can be exposed to frontend")
@field_validator('value_type')
@field_validator("value_type")
@classmethod
def validate_value_type(cls, v):
allowed = ['string', 'integer', 'boolean', 'json', 'float']
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')
@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")
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
@@ -160,12 +174,14 @@ class AdminSettingResponse(BaseModel):
class AdminSettingUpdate(BaseModel):
"""Update admin setting value."""
value: str
description: Optional[str] = None
class AdminSettingListResponse(BaseModel):
"""List of settings by category."""
settings: List[AdminSettingResponse]
total: int
category: Optional[str] = None
@@ -175,8 +191,10 @@ class AdminSettingListResponse(BaseModel):
# 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)
@@ -185,18 +203,25 @@ class PlatformAlertCreate(BaseModel):
affected_systems: Optional[List[str]] = None
auto_generated: bool = Field(default=True)
@field_validator('severity')
@field_validator("severity")
@classmethod
def validate_severity(cls, v):
allowed = ['info', 'warning', 'error', 'critical']
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')
@field_validator("alert_type")
@classmethod
def validate_alert_type(cls, v):
allowed = ['security', 'performance', 'capacity', 'integration', 'database', 'system']
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
@@ -204,6 +229,7 @@ class PlatformAlertCreate(BaseModel):
class PlatformAlertResponse(BaseModel):
"""Platform alert response."""
id: int
alert_type: str
severity: str
@@ -226,12 +252,14 @@ class PlatformAlertResponse(BaseModel):
class PlatformAlertResolve(BaseModel):
"""Resolve platform alert."""
is_resolved: bool = True
resolution_notes: Optional[str] = None
class PlatformAlertListResponse(BaseModel):
"""Paginated list of platform alerts."""
alerts: List[PlatformAlertResponse]
total: int
active_count: int
@@ -244,17 +272,19 @@ class PlatformAlertListResponse(BaseModel):
# 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: Optional[str] = Field(None, description="Reason for bulk action")
@field_validator('action')
@field_validator("action")
@classmethod
def validate_action(cls, v):
allowed = ['activate', 'deactivate', 'verify', 'unverify', 'delete']
allowed = ["activate", "deactivate", "verify", "unverify", "delete"]
if v not in allowed:
raise ValueError(f"Action must be one of: {', '.join(allowed)}")
return v
@@ -262,6 +292,7 @@ class BulkVendorAction(BaseModel):
class BulkVendorActionResponse(BaseModel):
"""Response for bulk vendor actions."""
successful: List[int]
failed: Dict[int, str] # vendor_id -> error_message
total_processed: int
@@ -271,15 +302,16 @@ class BulkVendorActionResponse(BaseModel):
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: Optional[str] = None
@field_validator('action')
@field_validator("action")
@classmethod
def validate_action(cls, v):
allowed = ['activate', 'deactivate', 'delete']
allowed = ["activate", "deactivate", "delete"]
if v not in allowed:
raise ValueError(f"Action must be one of: {', '.join(allowed)}")
return v
@@ -287,6 +319,7 @@ class BulkUserAction(BaseModel):
class BulkUserActionResponse(BaseModel):
"""Response for bulk user actions."""
successful: List[int]
failed: Dict[int, str]
total_processed: int
@@ -298,8 +331,10 @@ class BulkUserActionResponse(BaseModel):
# ADMIN DASHBOARD SCHEMAS
# ============================================================================
class AdminDashboardStats(BaseModel):
"""Comprehensive admin dashboard statistics."""
platform: Dict[str, Any]
users: Dict[str, Any]
vendors: Dict[str, Any]
@@ -317,8 +352,10 @@ class AdminDashboardStats(BaseModel):
# SYSTEM HEALTH SCHEMAS
# ============================================================================
class ComponentHealthStatus(BaseModel):
"""Health status for a system component."""
status: str # healthy, degraded, unhealthy
response_time_ms: Optional[float] = None
error_message: Optional[str] = None
@@ -328,6 +365,7 @@ class ComponentHealthStatus(BaseModel):
class SystemHealthResponse(BaseModel):
"""System health check response."""
overall_status: str # healthy, degraded, critical
database: ComponentHealthStatus
redis: ComponentHealthStatus
@@ -342,8 +380,10 @@ class SystemHealthResponse(BaseModel):
# ADMIN SESSION SCHEMAS
# ============================================================================
class AdminSessionResponse(BaseModel):
"""Admin session information."""
id: int
admin_user_id: int
admin_username: Optional[str] = None
@@ -360,6 +400,7 @@ class AdminSessionResponse(BaseModel):
class AdminSessionListResponse(BaseModel):
"""List of admin sessions."""
sessions: List[AdminSessionResponse]
total: int
active_count: int

View File

@@ -17,7 +17,9 @@ class UserRegister(BaseModel):
@classmethod
def validate_username(cls, v):
if not re.match(r"^[a-zA-Z0-9_]+$", v):
raise ValueError("Username must contain only letters, numbers, or underscores")
raise ValueError(
"Username must contain only letters, numbers, or underscores"
)
return v.lower().strip()
@field_validator("password")
@@ -31,7 +33,9 @@ class UserRegister(BaseModel):
class UserLogin(BaseModel):
email_or_username: str = Field(..., description="Username or email address")
password: str = Field(..., description="Password")
vendor_code: Optional[str] = Field(None, description="Optional vendor code for context")
vendor_code: Optional[str] = Field(
None, description="Optional vendor code for context"
)
@field_validator("email_or_username")
@classmethod

View File

@@ -5,21 +5,24 @@ Pydantic schemas for shopping cart operations.
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, Field, ConfigDict
from pydantic import BaseModel, ConfigDict, Field
# ============================================================================
# Request Schemas
# ============================================================================
class AddToCartRequest(BaseModel):
"""Request model for adding items to cart."""
product_id: int = Field(..., description="Product ID to add", gt=0)
quantity: int = Field(1, ge=1, description="Quantity to add")
class UpdateCartItemRequest(BaseModel):
"""Request model for updating cart item quantity."""
quantity: int = Field(..., ge=1, description="New quantity (must be >= 1)")
@@ -27,23 +30,30 @@ class UpdateCartItemRequest(BaseModel):
# Response Schemas
# ============================================================================
class CartItemResponse(BaseModel):
"""Response model for a single cart item."""
model_config = ConfigDict(from_attributes=True)
product_id: int = Field(..., description="Product ID")
product_name: str = Field(..., description="Product name")
quantity: int = Field(..., description="Quantity in cart")
price: float = Field(..., description="Price per unit when added to cart")
line_total: float = Field(..., description="Total price for this line (price * quantity)")
line_total: float = Field(
..., description="Total price for this line (price * quantity)"
)
image_url: Optional[str] = Field(None, description="Product image URL")
class CartResponse(BaseModel):
"""Response model for shopping cart."""
vendor_id: int = Field(..., description="Vendor ID")
session_id: str = Field(..., description="Shopping session ID")
items: List[CartItemResponse] = Field(default_factory=list, description="Cart items")
items: List[CartItemResponse] = Field(
default_factory=list, description="Cart items"
)
subtotal: float = Field(..., description="Subtotal of all items")
total: float = Field(..., description="Total amount (currently same as subtotal)")
item_count: int = Field(..., description="Total number of items in cart")
@@ -63,18 +73,22 @@ class CartResponse(BaseModel):
items=items,
subtotal=cart_dict["subtotal"],
total=cart_dict["total"],
item_count=len(items)
item_count=len(items),
)
class CartOperationResponse(BaseModel):
"""Response model for cart operations (add, update, remove)."""
message: str = Field(..., description="Operation result message")
product_id: int = Field(..., description="Product ID affected")
quantity: Optional[int] = Field(None, description="New quantity (for add/update operations)")
quantity: Optional[int] = Field(
None, description="New quantity (for add/update operations)"
)
class ClearCartResponse(BaseModel):
"""Response model for clearing cart."""
message: str = Field(..., description="Operation result message")
items_removed: int = Field(..., description="Number of items removed from cart")

View File

@@ -5,35 +5,34 @@ Pydantic schema for customer-related operations.
from datetime import datetime
from decimal import Decimal
from typing import Optional, Dict, Any, List
from pydantic import BaseModel, EmailStr, Field, field_validator
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, EmailStr, Field, field_validator
# ============================================================================
# Customer Registration & Authentication
# ============================================================================
class CustomerRegister(BaseModel):
"""Schema for customer registration."""
email: EmailStr = Field(..., description="Customer email address")
password: str = Field(
...,
min_length=8,
description="Password (minimum 8 characters)"
..., min_length=8, description="Password (minimum 8 characters)"
)
first_name: str = Field(..., min_length=1, max_length=100)
last_name: str = Field(..., min_length=1, max_length=100)
phone: Optional[str] = Field(None, max_length=50)
marketing_consent: bool = Field(default=False)
@field_validator('email')
@field_validator("email")
@classmethod
def email_lowercase(cls, v: str) -> str:
"""Convert email to lowercase."""
return v.lower()
@field_validator('password')
@field_validator("password")
@classmethod
def password_strength(cls, v: str) -> str:
"""Validate password strength."""
@@ -55,7 +54,7 @@ class CustomerUpdate(BaseModel):
phone: Optional[str] = Field(None, max_length=50)
marketing_consent: Optional[bool] = None
@field_validator('email')
@field_validator("email")
@classmethod
def email_lowercase(cls, v: Optional[str]) -> Optional[str]:
"""Convert email to lowercase."""
@@ -66,6 +65,7 @@ class CustomerUpdate(BaseModel):
# Customer Response
# ============================================================================
class CustomerResponse(BaseModel):
"""Schema for customer response (excludes password)."""
@@ -84,9 +84,7 @@ class CustomerResponse(BaseModel):
created_at: datetime
updated_at: datetime
model_config = {
"from_attributes": True
}
model_config = {"from_attributes": True}
@property
def full_name(self) -> str:
@@ -110,6 +108,7 @@ class CustomerListResponse(BaseModel):
# Customer Address
# ============================================================================
class CustomerAddressCreate(BaseModel):
"""Schema for creating customer address."""
@@ -159,14 +158,14 @@ class CustomerAddressResponse(BaseModel):
created_at: datetime
updated_at: datetime
model_config = {
"from_attributes": True
}
model_config = {"from_attributes": True}
# ============================================================================
# Customer Preferences
# ============================================================================
class CustomerPreferencesUpdate(BaseModel):
"""Schema for updating customer preferences."""

View File

@@ -1,6 +1,7 @@
# models/schema/inventory.py
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field
@@ -11,16 +12,21 @@ class InventoryBase(BaseModel):
class InventoryCreate(InventoryBase):
"""Set exact inventory quantity (replaces existing)."""
quantity: int = Field(..., description="Exact inventory quantity", ge=0)
class InventoryAdjust(InventoryBase):
"""Add or remove inventory quantity."""
quantity: int = Field(..., description="Quantity to add (positive) or remove (negative)")
quantity: int = Field(
..., description="Quantity to add (positive) or remove (negative)"
)
class InventoryUpdate(BaseModel):
"""Update inventory fields."""
quantity: Optional[int] = Field(None, ge=0)
reserved_quantity: Optional[int] = Field(None, ge=0)
location: Optional[str] = None
@@ -28,6 +34,7 @@ class InventoryUpdate(BaseModel):
class InventoryReserve(BaseModel):
"""Reserve inventory for orders."""
product_id: int
location: str
quantity: int = Field(..., gt=0)
@@ -60,6 +67,7 @@ class InventoryLocationResponse(BaseModel):
class ProductInventorySummary(BaseModel):
"""Inventory summary for a product."""
product_id: int
vendor_id: int
product_sku: Optional[str]

View File

@@ -1 +1 @@
# Marketplace import job models
# Marketplace import job models

View File

@@ -1,5 +1,6 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator
@@ -8,9 +9,12 @@ class MarketplaceImportJobRequest(BaseModel):
Note: vendor_id is injected by middleware, not from request body.
"""
source_url: str = Field(..., description="URL to CSV file from marketplace")
marketplace: str = Field(default="Letzshop", description="Marketplace name")
batch_size: Optional[int] = Field(1000, description="Processing batch size", ge=100, le=10000)
batch_size: Optional[int] = Field(
1000, description="Processing batch size", ge=100, le=10000
)
@field_validator("source_url")
@classmethod
@@ -28,6 +32,7 @@ class MarketplaceImportJobRequest(BaseModel):
class MarketplaceImportJobResponse(BaseModel):
"""Response schema for marketplace import job."""
model_config = ConfigDict(from_attributes=True)
job_id: int
@@ -55,6 +60,7 @@ class MarketplaceImportJobResponse(BaseModel):
class MarketplaceImportJobListResponse(BaseModel):
"""Response schema for list of import jobs."""
jobs: list[MarketplaceImportJobResponse]
total: int
skip: int
@@ -63,6 +69,7 @@ class MarketplaceImportJobListResponse(BaseModel):
class MarketplaceImportJobStatusUpdate(BaseModel):
"""Schema for updating import job status (internal use)."""
status: str
imported_count: Optional[int] = None
updated_count: Optional[int] = None

View File

@@ -1,9 +1,12 @@
# models/schema/marketplace_products.py - Simplified validation
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field
from models.schema.inventory import ProductInventorySummary
class MarketplaceProductBase(BaseModel):
marketplace_product_id: Optional[str] = None
title: Optional[str] = None
@@ -45,27 +48,34 @@ class MarketplaceProductBase(BaseModel):
marketplace: Optional[str] = None
vendor_name: Optional[str] = None
class MarketplaceProductCreate(MarketplaceProductBase):
marketplace_product_id: str = Field(..., description="MarketplaceProduct identifier")
marketplace_product_id: str = Field(
..., description="MarketplaceProduct identifier"
)
title: str = Field(..., description="MarketplaceProduct title")
# Removed: min_length constraints and custom validators
# Service will handle empty string validation with proper domain exceptions
class MarketplaceProductUpdate(MarketplaceProductBase):
pass
class MarketplaceProductResponse(MarketplaceProductBase):
model_config = ConfigDict(from_attributes=True)
id: int
created_at: datetime
updated_at: datetime
class MarketplaceProductListResponse(BaseModel):
products: List[MarketplaceProductResponse]
total: int
skip: int
limit: int
class MarketplaceProductDetailResponse(BaseModel):
product: MarketplaceProductResponse
inventory_info: Optional[ProductInventorySummary] = None

View File

@@ -1 +1 @@
# Media/file management models
# Media/file management models

View File

@@ -1 +1 @@
# Monitoring models
# Monitoring models

View File

@@ -1 +1 @@
# Notification models
# Notification models

View File

@@ -4,23 +4,26 @@ Pydantic schema for order operations.
"""
from datetime import datetime
from typing import List, Optional
from decimal import Decimal
from pydantic import BaseModel, Field, ConfigDict
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field
# ============================================================================
# Order Item Schemas
# ============================================================================
class OrderItemCreate(BaseModel):
"""Schema for creating an order item."""
product_id: int
quantity: int = Field(..., ge=1)
class OrderItemResponse(BaseModel):
"""Schema for order item response."""
model_config = ConfigDict(from_attributes=True)
id: int
@@ -41,8 +44,10 @@ class OrderItemResponse(BaseModel):
# Order Address Schemas
# ============================================================================
class OrderAddressCreate(BaseModel):
"""Schema for order address (shipping/billing)."""
first_name: str = Field(..., min_length=1, max_length=100)
last_name: str = Field(..., min_length=1, max_length=100)
company: Optional[str] = Field(None, max_length=200)
@@ -55,6 +60,7 @@ class OrderAddressCreate(BaseModel):
class OrderAddressResponse(BaseModel):
"""Schema for order address response."""
model_config = ConfigDict(from_attributes=True)
id: int
@@ -73,8 +79,10 @@ class OrderAddressResponse(BaseModel):
# Order Create/Update Schemas
# ============================================================================
class OrderCreate(BaseModel):
"""Schema for creating an order."""
customer_id: Optional[int] = None # Optional for guest checkout
items: List[OrderItemCreate] = Field(..., min_length=1)
@@ -92,9 +100,9 @@ class OrderCreate(BaseModel):
class OrderUpdate(BaseModel):
"""Schema for updating order status."""
status: Optional[str] = Field(
None,
pattern="^(pending|processing|shipped|delivered|cancelled|refunded)$"
None, pattern="^(pending|processing|shipped|delivered|cancelled|refunded)$"
)
tracking_number: Optional[str] = None
internal_notes: Optional[str] = None
@@ -104,8 +112,10 @@ class OrderUpdate(BaseModel):
# Order Response Schemas
# ============================================================================
class OrderResponse(BaseModel):
"""Schema for order response."""
model_config = ConfigDict(from_attributes=True)
id: int
@@ -141,6 +151,7 @@ class OrderResponse(BaseModel):
class OrderDetailResponse(OrderResponse):
"""Schema for detailed order response with items and addresses."""
items: List[OrderItemResponse]
shipping_address: OrderAddressResponse
billing_address: OrderAddressResponse
@@ -148,6 +159,7 @@ class OrderDetailResponse(OrderResponse):
class OrderListResponse(BaseModel):
"""Schema for paginated order list."""
orders: List[OrderResponse]
total: int
skip: int

View File

@@ -1 +1 @@
# Payment models
# Payment models

View File

@@ -1,14 +1,20 @@
# models/schema/product.py
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field
from models.schema.marketplace_product import MarketplaceProductResponse
from models.schema.inventory import InventoryLocationResponse
from models.schema.marketplace_product import MarketplaceProductResponse
class ProductCreate(BaseModel):
marketplace_product_id: int = Field(..., description="MarketplaceProduct ID to add to vendor catalog")
product_id: Optional[str] = Field(None, description="Vendor's internal SKU/product ID")
marketplace_product_id: int = Field(
..., description="MarketplaceProduct ID to add to vendor catalog"
)
product_id: Optional[str] = Field(
None, description="Vendor's internal SKU/product ID"
)
price: Optional[float] = Field(None, ge=0)
sale_price: Optional[float] = Field(None, ge=0)
currency: Optional[str] = None
@@ -59,6 +65,7 @@ class ProductResponse(BaseModel):
class ProductDetailResponse(ProductResponse):
"""Product with full inventory details."""
inventory_locations: List[InventoryLocationResponse] = []

View File

@@ -1 +1 @@
# Search models
# Search models

View File

@@ -1,6 +1,6 @@
import re
from decimal import Decimal
from datetime import datetime
from decimal import Decimal
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
@@ -22,10 +22,12 @@ class MarketplaceStatsResponse(BaseModel):
unique_vendors: int
unique_brands: int
# ============================================================================
# Customer Statistics
# ============================================================================
class CustomerStatsResponse(BaseModel):
"""Schema for customer statistics."""
@@ -42,8 +44,10 @@ class CustomerStatsResponse(BaseModel):
# Order Statistics
# ============================================================================
class OrderStatsResponse(BaseModel):
"""Schema for order statistics."""
total_orders: int
pending_orders: int
processing_orders: int
@@ -53,13 +57,16 @@ class OrderStatsResponse(BaseModel):
total_revenue: Decimal
average_order_value: Decimal
# ============================================================================
# Vendor Statistics
# ============================================================================
class VendorStatsResponse(BaseModel):
"""Vendor statistics response schema."""
total: int = Field(..., description="Total number of vendors")
verified: int = Field(..., description="Number of verified vendors")
pending: int = Field(..., description="Number of pending verification vendors")
inactive: int = Field(..., description="Number of inactive vendors")
inactive: int = Field(..., description="Number of inactive vendors")

View File

@@ -10,33 +10,40 @@ This module defines request/response schemas for:
"""
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, EmailStr, Field, field_validator
from typing import List, Optional
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")
permissions: List[str] = Field(
default_factory=list, description="List of permission strings"
)
class RoleCreate(RoleBase):
"""Schema for creating a role."""
pass
class RoleUpdate(BaseModel):
"""Schema for updating a role."""
name: Optional[str] = Field(None, min_length=1, max_length=100)
permissions: Optional[List[str]] = None
class RoleResponse(RoleBase):
"""Schema for role response."""
id: int
vendor_id: int
created_at: datetime
@@ -48,6 +55,7 @@ class RoleResponse(RoleBase):
class RoleListResponse(BaseModel):
"""Schema for role list response."""
roles: List[RoleResponse]
total: int
@@ -56,8 +64,10 @@ class RoleListResponse(BaseModel):
# Team Member Schemas
# ============================================================================
class TeamMemberBase(BaseModel):
"""Base team member schema."""
email: EmailStr = Field(..., description="Team member email address")
first_name: Optional[str] = Field(None, max_length=100)
last_name: Optional[str] = Field(None, max_length=100)
@@ -65,30 +75,34 @@ class TeamMemberBase(BaseModel):
class TeamMemberInvite(TeamMemberBase):
"""Schema for inviting a team member."""
role_id: Optional[int] = Field(None, description="Role ID to assign (for preset roles)")
role_name: Optional[str] = Field(None, description="Role name (manager, staff, support, etc.)")
role_id: Optional[int] = Field(
None, description="Role ID to assign (for preset roles)"
)
role_name: Optional[str] = Field(
None, description="Role name (manager, staff, support, etc.)"
)
custom_permissions: Optional[List[str]] = Field(
None,
description="Custom permissions (overrides role preset)"
None, description="Custom permissions (overrides role preset)"
)
@field_validator('role_name')
@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']
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')
@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']:
if "role_name" not in values or not values["role_name"]:
raise ValueError(
"role_name is required when providing custom_permissions"
)
@@ -97,12 +111,14 @@ class TeamMemberInvite(TeamMemberBase):
class TeamMemberUpdate(BaseModel):
"""Schema for updating a team member."""
role_id: Optional[int] = Field(None, description="New role ID")
is_active: Optional[bool] = Field(None, description="Active status")
class TeamMemberResponse(BaseModel):
"""Schema for team member response."""
id: int = Field(..., description="User ID")
email: EmailStr
username: str
@@ -112,15 +128,18 @@ class TeamMemberResponse(BaseModel):
user_type: str = Field(..., description="'owner' or 'member'")
role_name: str = Field(..., description="Role name")
role_id: Optional[int]
permissions: List[str] = Field(default_factory=list, description="User's permissions")
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"
default=False, description="True if invitation not yet accepted"
)
invited_at: Optional[datetime] = Field(None, description="When invitation was sent")
accepted_at: Optional[datetime] = Field(None, description="When invitation was accepted")
accepted_at: Optional[datetime] = Field(
None, description="When invitation was accepted"
)
joined_at: datetime = Field(..., description="When user joined vendor")
class Config:
@@ -129,6 +148,7 @@ class TeamMemberResponse(BaseModel):
class TeamMemberListResponse(BaseModel):
"""Schema for team member list response."""
members: List[TeamMemberResponse]
total: int
active_count: int
@@ -139,19 +159,20 @@ class TeamMemberListResponse(BaseModel):
# Invitation Schemas
# ============================================================================
class InvitationAccept(BaseModel):
"""Schema for accepting a team invitation."""
invitation_token: str = Field(..., min_length=32, description="Invitation token from email")
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"
..., 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')
@field_validator("password")
def validate_password_strength(cls, v):
"""Validate password meets minimum requirements."""
if len(v) < 8:
@@ -172,18 +193,19 @@ class InvitationAccept(BaseModel):
class InvitationResponse(BaseModel):
"""Schema for invitation response."""
message: str
email: EmailStr
role: str
invitation_token: Optional[str] = Field(
None,
description="Token (only returned in dev/test environments)"
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")
@@ -194,8 +216,10 @@ class InvitationAcceptResponse(BaseModel):
# Team Statistics Schema
# ============================================================================
class TeamStatistics(BaseModel):
"""Schema for team statistics."""
total_members: int
active_members: int
inactive_members: int
@@ -203,8 +227,7 @@ class TeamStatistics(BaseModel):
owners: int
team_members: int
roles_breakdown: dict = Field(
default_factory=dict,
description="Count of members per role"
default_factory=dict, description="Count of members per role"
)
@@ -212,13 +235,18 @@ class TeamStatistics(BaseModel):
# 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")
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)
@@ -228,21 +256,27 @@ class BulkRemoveResponse(BaseModel):
# 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")
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
@@ -253,8 +287,10 @@ class UserPermissionsResponse(BaseModel):
# Error Response Schema
# ============================================================================
class TeamErrorResponse(BaseModel):
"""Schema for team operation errors."""
error_code: str
message: str
details: Optional[dict] = None

View File

@@ -15,7 +15,8 @@ Schemas include:
import re
from datetime import datetime
from typing import Dict, List, Optional, Any
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator
@@ -27,32 +28,26 @@ class VendorCreate(BaseModel):
...,
description="Unique vendor identifier (e.g., TECHSTORE)",
min_length=2,
max_length=50
max_length=50,
)
subdomain: str = Field(
...,
description="Unique subdomain for the vendor",
min_length=2,
max_length=100
..., description="Unique subdomain for the vendor", min_length=2, max_length=100
)
name: str = Field(
...,
description="Display name of the vendor",
min_length=2,
max_length=255
..., description="Display name of the vendor", min_length=2, max_length=255
)
description: Optional[str] = Field(None, description="Vendor description")
# Owner Information (Creates User Account)
owner_email: str = Field(
...,
description="Email for the vendor owner (used for login and authentication)"
description="Email for the vendor owner (used for login and authentication)",
)
# Business Contact Information (Vendor Fields)
contact_email: Optional[str] = Field(
None,
description="Public business contact email (defaults to owner_email if not provided)"
description="Public business contact email (defaults to owner_email if not provided)",
)
contact_phone: Optional[str] = Field(None, description="Contact phone number")
website: Optional[str] = Field(None, description="Website URL")
@@ -78,8 +73,10 @@ class VendorCreate(BaseModel):
@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")
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")
@@ -104,8 +101,7 @@ class VendorUpdate(BaseModel):
# Business Contact Information (Vendor Fields)
contact_email: Optional[str] = Field(
None,
description="Public business contact email"
None, description="Public business contact email"
)
contact_phone: Optional[str] = None
website: Optional[str] = None
@@ -142,6 +138,7 @@ class VendorUpdate(BaseModel):
class VendorResponse(BaseModel):
"""Standard schema for vendor response data."""
model_config = ConfigDict(from_attributes=True)
id: int
@@ -184,13 +181,9 @@ class VendorDetailResponse(VendorResponse):
"""
owner_email: str = Field(
...,
description="Email of the vendor owner (for login/authentication)"
)
owner_username: str = Field(
...,
description="Username of the vendor owner"
..., description="Email of the vendor owner (for login/authentication)"
)
owner_username: str = Field(..., description="Username of the vendor owner")
class VendorCreateResponse(VendorDetailResponse):
@@ -201,17 +194,14 @@ class VendorCreateResponse(VendorDetailResponse):
"""
temporary_password: str = Field(
...,
description="Temporary password for owner (SHOWN ONLY ONCE)"
)
login_url: Optional[str] = Field(
None,
description="URL for vendor owner to login"
..., description="Temporary password for owner (SHOWN ONLY ONCE)"
)
login_url: Optional[str] = Field(None, description="URL for vendor owner to login")
class VendorListResponse(BaseModel):
"""Schema for paginated vendor list."""
vendors: List[VendorResponse]
total: int
skip: int
@@ -220,6 +210,7 @@ class VendorListResponse(BaseModel):
class VendorSummary(BaseModel):
"""Lightweight vendor summary for dropdowns and quick references."""
model_config = ConfigDict(from_attributes=True)
id: int
@@ -239,20 +230,17 @@ class VendorTransferOwnership(BaseModel):
"""
new_owner_user_id: int = Field(
...,
description="ID of the user who will become the new owner",
gt=0
..., 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"
..., description="Must be true to confirm ownership transfer"
)
transfer_reason: Optional[str] = Field(
None,
max_length=500,
description="Reason for ownership transfer (for audit logs)"
description="Reason for ownership transfer (for audit logs)",
)
@field_validator("confirm_transfer")
@@ -273,12 +261,10 @@ class VendorTransferOwnershipResponse(BaseModel):
vendor_name: str
old_owner: Dict[str, Any] = Field(
...,
description="Information about the previous owner"
..., description="Information about the previous owner"
)
new_owner: Dict[str, Any] = Field(
...,
description="Information about the new owner"
..., description="Information about the new owner"
)
transferred_at: datetime

View File

@@ -12,7 +12,8 @@ Schemas include:
import re
from datetime import datetime
from typing import List, Optional, Dict
from typing import Dict, List, Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator
@@ -23,14 +24,13 @@ class VendorDomainCreate(BaseModel):
...,
description="Custom domain (e.g., myshop.com or shop.mybrand.com)",
min_length=3,
max_length=255
max_length=255,
)
is_primary: bool = Field(
default=False,
description="Set as primary domain for the vendor"
default=False, description="Set as primary domain for the vendor"
)
@field_validator('domain')
@field_validator("domain")
@classmethod
def validate_domain(cls, v: str) -> str:
"""Validate and normalize domain."""
@@ -44,20 +44,22 @@ class VendorDomainCreate(BaseModel):
domain = domain.lower().strip()
# Basic validation
if not domain or '/' in domain:
if not domain or "/" in domain:
raise ValueError("Invalid domain format")
if '.' not in domain:
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]
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}")
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])?)*$'
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")
@@ -75,6 +77,7 @@ class VendorDomainUpdate(BaseModel):
class VendorDomainResponse(BaseModel):
"""Standard schema for vendor domain response."""
model_config = ConfigDict(from_attributes=True)
id: int

View File

@@ -3,12 +3,14 @@
Pydantic schemas for vendor theme operations.
"""
from typing import Dict, Optional, List
from typing import Dict, List, Optional
from pydantic import BaseModel, Field
class VendorThemeColors(BaseModel):
"""Color scheme for vendor theme."""
primary: Optional[str] = Field(None, description="Primary brand color")
secondary: Optional[str] = Field(None, description="Secondary color")
accent: Optional[str] = Field(None, description="Accent/CTA color")
@@ -19,12 +21,14 @@ class VendorThemeColors(BaseModel):
class VendorThemeFonts(BaseModel):
"""Typography settings for vendor theme."""
heading: Optional[str] = Field(None, description="Font for headings")
body: Optional[str] = Field(None, description="Font for body text")
class VendorThemeBranding(BaseModel):
"""Branding assets for vendor theme."""
logo: Optional[str] = Field(None, description="Logo URL")
logo_dark: Optional[str] = Field(None, description="Dark mode logo URL")
favicon: Optional[str] = Field(None, description="Favicon URL")
@@ -33,36 +37,54 @@ class VendorThemeBranding(BaseModel):
class VendorThemeLayout(BaseModel):
"""Layout settings for vendor theme."""
style: Optional[str] = Field(None, description="Product layout style (grid, list, masonry)")
header: Optional[str] = Field(None, description="Header style (fixed, static, transparent)")
product_card: Optional[str] = Field(None, description="Product card style (modern, classic, minimal)")
style: Optional[str] = Field(
None, description="Product layout style (grid, list, masonry)"
)
header: Optional[str] = Field(
None, description="Header style (fixed, static, transparent)"
)
product_card: Optional[str] = Field(
None, description="Product card style (modern, classic, minimal)"
)
class VendorThemeUpdate(BaseModel):
"""Schema for updating vendor theme (partial updates allowed)."""
theme_name: Optional[str] = Field(None, description="Theme preset name")
colors: Optional[Dict[str, str]] = Field(None, description="Color scheme")
fonts: Optional[Dict[str, str]] = Field(None, description="Font settings")
branding: Optional[Dict[str, Optional[str]]] = Field(None, description="Branding assets")
branding: Optional[Dict[str, Optional[str]]] = Field(
None, description="Branding assets"
)
layout: Optional[Dict[str, str]] = Field(None, description="Layout settings")
custom_css: Optional[str] = Field(None, description="Custom CSS rules")
social_links: Optional[Dict[str, str]] = Field(None, description="Social media links")
social_links: Optional[Dict[str, str]] = 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, Optional[str]] = Field(..., description="Branding assets")
layout: Dict[str, str] = Field(..., description="Layout settings")
social_links: Optional[Dict[str, str]] = Field(default_factory=dict, description="Social links")
social_links: Optional[Dict[str, str]] = Field(
default_factory=dict, description="Social links"
)
custom_css: Optional[str] = Field(None, description="Custom CSS")
css_variables: Optional[Dict[str, str]] = Field(None, description="CSS custom properties")
css_variables: Optional[Dict[str, str]] = 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")
@@ -75,10 +97,12 @@ class ThemePresetPreview(BaseModel):
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")