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:
@@ -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__ = [
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1 +1 @@
|
||||
# AuditLog, DataExportLog models
|
||||
# AuditLog, DataExportLog models
|
||||
|
||||
@@ -1 +1 @@
|
||||
# BackupLog, RestoreLog models
|
||||
# BackupLog, RestoreLog models
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1 +1 @@
|
||||
# PlatformConfig, VendorConfig, FeatureFlag models
|
||||
# PlatformConfig, VendorConfig, FeatureFlag models
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
|
||||
@@ -1 +1 @@
|
||||
# MarketplaceImportJob model
|
||||
# MarketplaceImportJob model
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
# MediaFile, ProductMedia models
|
||||
# MediaFile, ProductMedia models
|
||||
|
||||
@@ -1 +1 @@
|
||||
# PerformanceMetric, ErrorLog, SystemAlert models
|
||||
# PerformanceMetric, ErrorLog, SystemAlert models
|
||||
|
||||
@@ -1 +1 @@
|
||||
# NotificationTemplate, NotificationQueue, NotificationLog models
|
||||
# NotificationTemplate, NotificationQueue, NotificationLog models
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Payment, PaymentMethod, VendorPaymentConfig models
|
||||
# Payment, PaymentMethod, VendorPaymentConfig models
|
||||
|
||||
@@ -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__ = (
|
||||
|
||||
@@ -1 +1 @@
|
||||
# SearchIndex, SearchQuery models
|
||||
# SearchIndex, SearchQuery models
|
||||
|
||||
@@ -1 +1 @@
|
||||
# TaskLog model
|
||||
# TaskLog model
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Marketplace import job models
|
||||
# Marketplace import job models
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Media/file management models
|
||||
# Media/file management models
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Monitoring models
|
||||
# Monitoring models
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Notification models
|
||||
# Notification models
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Payment models
|
||||
# Payment models
|
||||
|
||||
@@ -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] = []
|
||||
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Search models
|
||||
# Search models
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user