style: apply black and isort formatting across entire codebase

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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