refactor: clean up legacy models and migrate remaining schemas

Delete empty stub files from models/database/:
- audit.py, backup.py, configuration.py, monitoring.py
- notification.py, payment.py, search.py, task.py

Delete re-export files:
- models/database/subscription.py → app.modules.billing.models
- models/database/architecture_scan.py → app.modules.dev_tools.models
- models/database/test_run.py → app.modules.dev_tools.models
- models/schema/subscription.py → app.modules.billing.schemas
- models/schema/marketplace.py (empty)
- models/schema/monitoring.py (empty)

Migrate schemas to canonical module locations:
- billing.py → app/modules/billing/schemas/
- vendor_product.py → app/modules/catalog/schemas/
- homepage_sections.py → app/modules/cms/schemas/

Keep as CORE (framework-level, used everywhere):
- models/schema/: admin, auth, base, company, email, image, media, team, vendor*
- models/database/: admin*, base, company, email, feature, media, platform*, user, vendor*

Update 30+ files to use canonical import locations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 18:45:46 +01:00
parent 1ef50893a1
commit b9f08b853f
49 changed files with 152 additions and 220 deletions

View File

@@ -19,7 +19,7 @@ from app.services.code_quality_service import (
code_quality_service,
)
from app.tasks.code_quality_tasks import execute_code_quality_scan
from models.database.architecture_scan import ArchitectureScan
from app.modules.dev_tools.models import ArchitectureScan
from models.database.user import User
from app.modules.analytics.schemas import CodeQualityDashboardStatsResponse

View File

@@ -19,7 +19,7 @@ from app.core.database import get_db
from app.services.admin_subscription_service import admin_subscription_service
from app.services.subscription_service import subscription_service
from models.database.user import User
from models.schema.billing import (
from app.modules.billing.schemas import (
BillingHistoryListResponse,
BillingHistoryWithVendor,
SubscriptionStatsResponse,

View File

@@ -22,7 +22,7 @@ from app.core.database import get_db
from app.services.subscription_service import subscription_service
from app.services.vendor_product_service import vendor_product_service
from models.database.user import User
from models.schema.vendor_product import (
from app.modules.catalog.schemas import (
CatalogVendor,
CatalogVendorsResponse,
RemoveProductResponse,

View File

@@ -15,7 +15,7 @@ from sqlalchemy.orm import Session
from app.core.database import get_db
from app.exceptions import ResourceNotFoundException
from app.services.platform_pricing_service import platform_pricing_service
from models.database.subscription import TierCode
from app.modules.billing.models import TierCode
router = APIRouter()
@@ -188,7 +188,7 @@ def get_tiers(db: Session = Depends(get_db)) -> list[TierResponse]:
return [_tier_to_response(tier, is_from_db=True) for tier in db_tiers]
# Fallback to hardcoded tiers
from models.database.subscription import TIER_LIMITS
from app.modules.billing.models import TIER_LIMITS
tiers = []
for tier_code in TIER_LIMITS:

View File

@@ -26,7 +26,7 @@ from app.modules.catalog.schemas import (
ProductToggleResponse,
ProductUpdate,
)
from models.schema.vendor_product import (
from app.modules.catalog.schemas import (
VendorDirectProductCreate,
VendorProductCreateResponse,
)

View File

@@ -15,7 +15,7 @@ from datetime import datetime, timezone
import stripe
from sqlalchemy.orm import Session
from models.database.subscription import (
from app.modules.billing.models import (
AddOnProduct,
BillingHistory,
StripeWebhookEvent,

View File

@@ -17,7 +17,7 @@ from sqlalchemy import func
from sqlalchemy.orm import Session
from app.modules.catalog.models import Product
from models.database.subscription import SubscriptionTier, VendorSubscription
from app.modules.billing.models import SubscriptionTier, VendorSubscription
from models.database.vendor import VendorUser
logger = logging.getLogger(__name__)

View File

@@ -19,7 +19,7 @@ from app.api.deps import get_current_admin_api, require_module_access
from app.core.database import get_db
from app.modules.billing.services import admin_subscription_service, subscription_service
from models.database.user import User
from models.schema.billing import (
from app.modules.billing.schemas import (
BillingHistoryListResponse,
BillingHistoryWithVendor,
SubscriptionStatsResponse,

View File

@@ -32,24 +32,70 @@ from app.modules.billing.schemas.subscription import (
CanAddTeamMemberResponse,
FeatureCheckResponse,
)
from app.modules.billing.schemas.billing import (
# Subscription Tier Admin schemas
SubscriptionTierBase,
SubscriptionTierCreate,
SubscriptionTierUpdate,
SubscriptionTierResponse,
SubscriptionTierListResponse,
# Vendor Subscription schemas
VendorSubscriptionResponse,
VendorSubscriptionWithVendor,
VendorSubscriptionListResponse,
VendorSubscriptionCreate,
VendorSubscriptionUpdate,
# Billing History schemas
BillingHistoryResponse,
BillingHistoryWithVendor,
BillingHistoryListResponse,
# Checkout & Portal schemas
CheckoutRequest,
CheckoutResponse,
PortalSessionResponse,
# Stats schemas
SubscriptionStatsResponse,
)
__all__ = [
# Tier schemas
# Tier schemas (subscription.py)
"TierFeatures",
"TierLimits",
"TierInfo",
# Subscription CRUD schemas
# Subscription CRUD schemas (subscription.py)
"SubscriptionCreate",
"SubscriptionUpdate",
"SubscriptionResponse",
# Usage schemas
# Usage schemas (subscription.py)
"SubscriptionUsage",
"UsageSummary",
"SubscriptionStatusResponse",
# Limit check schemas
# Limit check schemas (subscription.py)
"LimitCheckResult",
"CanCreateOrderResponse",
"CanAddProductResponse",
"CanAddTeamMemberResponse",
"FeatureCheckResponse",
# Subscription Tier Admin schemas (billing.py)
"SubscriptionTierBase",
"SubscriptionTierCreate",
"SubscriptionTierUpdate",
"SubscriptionTierResponse",
"SubscriptionTierListResponse",
# Vendor Subscription schemas (billing.py)
"VendorSubscriptionResponse",
"VendorSubscriptionWithVendor",
"VendorSubscriptionListResponse",
"VendorSubscriptionCreate",
"VendorSubscriptionUpdate",
# Billing History schemas (billing.py)
"BillingHistoryResponse",
"BillingHistoryWithVendor",
"BillingHistoryListResponse",
# Checkout & Portal schemas (billing.py)
"CheckoutRequest",
"CheckoutResponse",
"PortalSessionResponse",
# Stats schemas (billing.py)
"SubscriptionStatsResponse",
]

View File

@@ -0,0 +1,300 @@
# app/modules/billing/schemas/billing.py
"""
Pydantic schemas for billing and subscription operations.
Used for both vendor billing endpoints and admin subscription management.
"""
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
# ============================================================================
# Subscription Tier Schemas
# ============================================================================
class SubscriptionTierBase(BaseModel):
"""Base schema for subscription tier."""
code: str = Field(..., min_length=1, max_length=30)
name: str = Field(..., min_length=1, max_length=100)
description: str | None = None
price_monthly_cents: int = Field(..., ge=0)
price_annual_cents: int | None = Field(None, ge=0)
orders_per_month: int | None = Field(None, ge=0)
products_limit: int | None = Field(None, ge=0)
team_members: int | None = Field(None, ge=0)
order_history_months: int | None = Field(None, ge=0)
features: list[str] = Field(default_factory=list)
stripe_product_id: str | None = None
stripe_price_monthly_id: str | None = None
stripe_price_annual_id: str | None = None
display_order: int = 0
is_active: bool = True
is_public: bool = True
class SubscriptionTierCreate(SubscriptionTierBase):
"""Schema for creating a subscription tier."""
pass
class SubscriptionTierUpdate(BaseModel):
"""Schema for updating a subscription tier."""
name: str | None = None
description: str | None = None
price_monthly_cents: int | None = Field(None, ge=0)
price_annual_cents: int | None = Field(None, ge=0)
orders_per_month: int | None = None
products_limit: int | None = None
team_members: int | None = None
order_history_months: int | None = None
features: list[str] | None = None
stripe_product_id: str | None = None
stripe_price_monthly_id: str | None = None
stripe_price_annual_id: str | None = None
display_order: int | None = None
is_active: bool | None = None
is_public: bool | None = None
class SubscriptionTierResponse(SubscriptionTierBase):
"""Schema for subscription tier response."""
model_config = ConfigDict(from_attributes=True)
id: int
created_at: datetime
updated_at: datetime
# Computed fields for display
@property
def price_monthly_display(self) -> str:
"""Format monthly price for display."""
return f"{self.price_monthly_cents / 100:.2f}"
@property
def price_annual_display(self) -> str | None:
"""Format annual price for display."""
if self.price_annual_cents is None:
return None
return f"{self.price_annual_cents / 100:.2f}"
class SubscriptionTierListResponse(BaseModel):
"""Response for listing subscription tiers."""
tiers: list[SubscriptionTierResponse]
total: int
# ============================================================================
# Vendor Subscription Schemas
# ============================================================================
class VendorSubscriptionResponse(BaseModel):
"""Schema for vendor subscription response."""
model_config = ConfigDict(from_attributes=True)
id: int
vendor_id: int
tier: str
status: str
# Period info
period_start: datetime
period_end: datetime
is_annual: bool
trial_ends_at: datetime | None = None
# Usage
orders_this_period: int
orders_limit_reached_at: datetime | None = None
# Limits (effective)
orders_limit: int | None = None
products_limit: int | None = None
team_members_limit: int | None = None
# Custom overrides
custom_orders_limit: int | None = None
custom_products_limit: int | None = None
custom_team_limit: int | None = None
# Stripe
stripe_customer_id: str | None = None
stripe_subscription_id: str | None = None
# Cancellation
cancelled_at: datetime | None = None
cancellation_reason: str | None = None
# Timestamps
created_at: datetime
updated_at: datetime
class VendorSubscriptionWithVendor(VendorSubscriptionResponse):
"""Subscription response with vendor info."""
vendor_name: str
vendor_code: str
# Usage counts (for admin display)
products_count: int | None = None
team_count: int | None = None
class VendorSubscriptionListResponse(BaseModel):
"""Response for listing vendor subscriptions."""
subscriptions: list[VendorSubscriptionWithVendor]
total: int
page: int
per_page: int
pages: int
class VendorSubscriptionCreate(BaseModel):
"""Schema for admin creating a vendor subscription."""
tier: str = "essential"
status: str = "trial"
trial_days: int = 14
is_annual: bool = False
class VendorSubscriptionUpdate(BaseModel):
"""Schema for admin updating a vendor subscription."""
tier: str | None = None
status: str | None = None
custom_orders_limit: int | None = None
custom_products_limit: int | None = None
custom_team_limit: int | None = None
trial_ends_at: datetime | None = None
cancellation_reason: str | None = None
# ============================================================================
# Billing History Schemas
# ============================================================================
class BillingHistoryResponse(BaseModel):
"""Schema for billing history entry."""
model_config = ConfigDict(from_attributes=True)
id: int
vendor_id: int
stripe_invoice_id: str | None = None
invoice_number: str | None = None
invoice_date: datetime
due_date: datetime | None = None
# Amounts
subtotal_cents: int
tax_cents: int
total_cents: int
amount_paid_cents: int
currency: str = "EUR"
# Status
status: str
# URLs
invoice_pdf_url: str | None = None
hosted_invoice_url: str | None = None
# Description
description: str | None = None
# Timestamps
created_at: datetime
@property
def total_display(self) -> str:
"""Format total for display."""
return f"{self.total_cents / 100:.2f}"
class BillingHistoryWithVendor(BillingHistoryResponse):
"""Billing history with vendor info."""
vendor_name: str
vendor_code: str
class BillingHistoryListResponse(BaseModel):
"""Response for listing billing history."""
invoices: list[BillingHistoryWithVendor]
total: int
page: int
per_page: int
pages: int
# ============================================================================
# Checkout & Portal Schemas
# ============================================================================
class CheckoutRequest(BaseModel):
"""Request for creating checkout session."""
tier_code: str
is_annual: bool = False
class CheckoutResponse(BaseModel):
"""Response with checkout session URL."""
checkout_url: str
session_id: str
class PortalSessionResponse(BaseModel):
"""Response with customer portal URL."""
portal_url: str
# ============================================================================
# Subscription Stats Schemas
# ============================================================================
class SubscriptionStatsResponse(BaseModel):
"""Subscription statistics for admin dashboard."""
total_subscriptions: int
active_count: int
trial_count: int
past_due_count: int
cancelled_count: int
expired_count: int
# By tier
tier_distribution: dict[str, int]
# Revenue
mrr_cents: int # Monthly recurring revenue
arr_cents: int # Annual recurring revenue
@property
def mrr_display(self) -> str:
"""Format MRR for display."""
return f"{self.mrr_cents / 100:,.2f}"
@property
def arr_display(self) -> str:
"""Format ARR for display."""
return f"{self.arr_cents / 100:,.2f}"

View File

@@ -15,6 +15,23 @@ from app.modules.catalog.schemas.product import (
ProductDeleteResponse,
ProductToggleResponse,
)
from app.modules.catalog.schemas.vendor_product import (
# List/Detail schemas
VendorProductListItem,
VendorProductListResponse,
VendorProductStats,
VendorProductDetail,
# Catalog vendor schemas
CatalogVendor,
CatalogVendorsResponse,
# CRUD schemas
TranslationUpdate,
VendorProductCreate,
VendorDirectProductCreate,
VendorProductUpdate,
VendorProductCreateResponse,
RemoveProductResponse,
)
__all__ = [
# Catalog browsing schemas (storefront)
@@ -29,4 +46,17 @@ __all__ = [
"ProductListResponse",
"ProductDeleteResponse",
"ProductToggleResponse",
# Vendor Product schemas (admin)
"VendorProductListItem",
"VendorProductListResponse",
"VendorProductStats",
"VendorProductDetail",
"CatalogVendor",
"CatalogVendorsResponse",
"TranslationUpdate",
"VendorProductCreate",
"VendorDirectProductCreate",
"VendorProductUpdate",
"VendorProductCreateResponse",
"RemoveProductResponse",
]

View File

@@ -0,0 +1,247 @@
# app/modules/catalog/schemas/vendor_product.py
"""
Pydantic schemas for vendor product catalog operations.
Used by admin vendor product endpoints for:
- Product listing and filtering
- Product statistics
- Product detail views
- Catalog vendor listings
"""
from pydantic import BaseModel, ConfigDict
class VendorProductListItem(BaseModel):
"""Product item for vendor catalog list view."""
model_config = ConfigDict(from_attributes=True)
id: int
vendor_id: int
vendor_name: str | None = None
vendor_code: str | None = None
marketplace_product_id: int | None = None
vendor_sku: str | None = None
title: str | None = None
brand: str | None = None
price: float | None = None
currency: str | None = None
effective_price: float | None = None
effective_currency: str | None = None
is_active: bool | None = None
is_featured: bool | None = None
is_digital: bool | None = None
image_url: str | None = None
source_marketplace: str | None = None
source_vendor: str | None = None
created_at: str | None = None
updated_at: str | None = None
class VendorProductListResponse(BaseModel):
"""Paginated vendor product list response."""
products: list[VendorProductListItem]
total: int
skip: int
limit: int
class VendorProductStats(BaseModel):
"""Vendor product statistics."""
total: int
active: int
inactive: int
featured: int
digital: int
physical: int
by_vendor: dict[str, int]
class CatalogVendor(BaseModel):
"""Vendor with products in catalog."""
id: int
name: str
vendor_code: str
class CatalogVendorsResponse(BaseModel):
"""Response for catalog vendors list."""
vendors: list[CatalogVendor]
class VendorProductDetail(BaseModel):
"""Detailed vendor product information.
Products are independent entities - all fields are populated at creation.
Source values are kept for "view original source" comparison only.
"""
id: int
vendor_id: int
vendor_name: str | None = None
vendor_code: str | None = None
marketplace_product_id: int | None = None # Optional for direct product creation
vendor_sku: str | None = None
# Product identifiers
gtin: str | None = None
gtin_type: str | None = None # ean13, ean8, upc, isbn, etc.
# Product fields with source comparison
price: float | None = None
price_cents: int | None = None
price_source: float | None = None # For "view original source" feature
sale_price: float | None = None
sale_price_cents: int | None = None
sale_price_source: float | None = None
currency: str | None = None
currency_source: str | None = None
brand: str | None = None
brand_source: str | None = None
condition: str | None = None
condition_source: str | None = None
availability: str | None = None
availability_source: str | None = None
primary_image_url: str | None = None
primary_image_url_source: str | None = None
additional_images: list[str] | None = None
is_digital: bool | None = None
product_type: str | None = None
# Vendor-specific fields
is_featured: bool | None = None
is_active: bool | None = None
display_order: int | None = None
min_quantity: int | None = None
max_quantity: int | None = None
# Supplier tracking
supplier: str | None = None
supplier_product_id: str | None = None
cost: float | None = None # What vendor pays to acquire product
margin_percent: float | None = None
# Tax/profit info
tax_rate_percent: int | None = None
net_price: float | None = None
vat_amount: float | None = None
profit: float | None = None
profit_margin_percent: float | None = None
# Digital fulfillment
download_url: str | None = None
license_type: str | None = None
fulfillment_email_template: str | None = None
# Source info
source_marketplace: str | None = None
source_vendor: str | None = None
source_gtin: str | None = None
source_sku: str | None = None
# Translations
marketplace_translations: dict | None = None
vendor_translations: dict | None = None
# Convenience fields for UI display
title: str | None = None
description: str | None = None
image_url: str | None = None # Alias for primary_image_url
# Timestamps
created_at: str | None = None
updated_at: str | None = None
class RemoveProductResponse(BaseModel):
"""Response from product removal."""
message: str
class TranslationUpdate(BaseModel):
"""Translation data for a single language."""
title: str | None = None
description: str | None = None
class VendorProductCreate(BaseModel):
"""Schema for creating a vendor product (admin use - includes vendor_id)."""
vendor_id: int
# Translations by language code (en, fr, de, lu)
translations: dict[str, TranslationUpdate] | None = None
# Product identifiers
brand: str | None = None
vendor_sku: str | None = None
gtin: str | None = None
gtin_type: str | None = None # ean13, ean8, upc, isbn
# Pricing
price: float | None = None
sale_price: float | None = None
currency: str = "EUR"
tax_rate_percent: int | None = 17 # Default Luxembourg VAT
availability: str | None = None
# Images
primary_image_url: str | None = None
additional_images: list[str] | None = None
# Status
is_active: bool = True
is_featured: bool = False
is_digital: bool = False
class VendorDirectProductCreate(BaseModel):
"""Schema for vendor direct product creation (vendor_id from JWT token)."""
title: str
brand: str | None = None
vendor_sku: str | None = None
gtin: str | None = None
price: float | None = None
currency: str = "EUR"
availability: str | None = None
is_active: bool = True
is_featured: bool = False
description: str | None = None
class VendorProductUpdate(BaseModel):
"""Schema for updating a vendor product."""
# Translations by language code (en, fr, de, lu)
translations: dict[str, TranslationUpdate] | None = None
# Product identifiers
brand: str | None = None
vendor_sku: str | None = None
gtin: str | None = None
gtin_type: str | None = None # ean13, ean8, upc, isbn, etc.
# Pricing
price: float | None = None # Price incl. VAT in euros
sale_price: float | None = None # Optional sale price
currency: str | None = None
tax_rate_percent: int | None = None # 3, 8, 14, 17
availability: str | None = None # in_stock, out_of_stock, preorder, backorder
# Status
is_digital: bool | None = None
is_active: bool | None = None
is_featured: bool | None = None
# Images
primary_image_url: str | None = None
additional_images: list[str] | None = None
# Optional supplier info
supplier: str | None = None
cost: float | None = None # Cost in euros
class VendorProductCreateResponse(BaseModel):
"""Response from product creation."""
id: int
message: str

View File

@@ -8,7 +8,7 @@ from app.modules.cms.schemas.content_page import (
ContentPageCreate,
ContentPageUpdate,
ContentPageResponse,
HomepageSectionsResponse,
HomepageSectionsResponse as ContentPageHomepageSectionsResponse,
SectionUpdateResponse,
# Vendor schemas
VendorContentPageCreate,
@@ -18,19 +18,46 @@ from app.modules.cms.schemas.content_page import (
PublicContentPageResponse,
ContentPageListItem,
)
from app.modules.cms.schemas.homepage_sections import (
# Translatable text
TranslatableText,
# Section components
HeroButton,
HeroSection,
FeatureCard,
FeaturesSection,
PricingSection,
CTASection,
# Main structure
HomepageSections,
# API schemas
SectionUpdateRequest,
HomepageSectionsResponse,
)
__all__ = [
# Admin
# Content Page - Admin
"ContentPageCreate",
"ContentPageUpdate",
"ContentPageResponse",
"HomepageSectionsResponse",
"ContentPageHomepageSectionsResponse",
"SectionUpdateResponse",
# Vendor
# Content Page - Vendor
"VendorContentPageCreate",
"VendorContentPageUpdate",
"CMSUsageResponse",
# Public
# Content Page - Public
"PublicContentPageResponse",
"ContentPageListItem",
# Homepage Sections
"TranslatableText",
"HeroButton",
"HeroSection",
"FeatureCard",
"FeaturesSection",
"PricingSection",
"CTASection",
"HomepageSections",
"SectionUpdateRequest",
"HomepageSectionsResponse",
]

View File

@@ -0,0 +1,174 @@
"""
Homepage Section Schemas with Dynamic Multi-Language Support.
Language codes are NOT hardcoded - they come from platform.supported_languages.
The TranslatableText class stores translations as a dict where keys are language codes.
Example JSON structure:
{
"hero": {
"enabled": true,
"title": {"translations": {"fr": "Bienvenue", "en": "Welcome"}},
"subtitle": {"translations": {...}},
"buttons": [...]
},
"features": {...},
"pricing": {...},
"cta": {...}
}
"""
from pydantic import BaseModel, Field
from typing import Optional
class TranslatableText(BaseModel):
"""
Text field with translations stored as language-keyed dict.
Languages come from platform.supported_languages (not hardcoded).
Use .get(lang, default_lang) to retrieve translation with fallback.
"""
translations: dict[str, str] = Field(
default_factory=dict, description="Language code -> translated text mapping"
)
def get(self, lang: str, default_lang: str = "fr") -> str:
"""Get translation with fallback to default language."""
return self.translations.get(lang) or self.translations.get(default_lang) or ""
def set(self, lang: str, text: str) -> None:
"""Set translation for a language."""
self.translations[lang] = text
def has_translation(self, lang: str) -> bool:
"""Check if translation exists for language."""
return bool(self.translations.get(lang))
class HeroButton(BaseModel):
"""Button in hero or CTA section."""
text: TranslatableText = Field(default_factory=TranslatableText)
url: str = ""
style: str = Field(default="primary", description="primary, secondary, outline")
class HeroSection(BaseModel):
"""Hero section configuration."""
enabled: bool = True
badge_text: Optional[TranslatableText] = None
title: TranslatableText = Field(default_factory=TranslatableText)
subtitle: TranslatableText = Field(default_factory=TranslatableText)
background_type: str = Field(
default="gradient", description="gradient, image, solid"
)
background_image: Optional[str] = None
buttons: list[HeroButton] = Field(default_factory=list)
class FeatureCard(BaseModel):
"""Single feature in features section."""
icon: str = ""
title: TranslatableText = Field(default_factory=TranslatableText)
description: TranslatableText = Field(default_factory=TranslatableText)
class FeaturesSection(BaseModel):
"""Features section configuration."""
enabled: bool = True
title: TranslatableText = Field(default_factory=TranslatableText)
subtitle: Optional[TranslatableText] = None
features: list[FeatureCard] = Field(default_factory=list)
layout: str = Field(default="grid", description="grid, list, cards")
class PricingSection(BaseModel):
"""Pricing section configuration."""
enabled: bool = True
title: TranslatableText = Field(default_factory=TranslatableText)
subtitle: Optional[TranslatableText] = None
use_subscription_tiers: bool = Field(
default=True, description="Pull pricing from subscription_tiers table dynamically"
)
class CTASection(BaseModel):
"""Call-to-action section configuration."""
enabled: bool = True
title: TranslatableText = Field(default_factory=TranslatableText)
subtitle: Optional[TranslatableText] = None
buttons: list[HeroButton] = Field(default_factory=list)
background_type: str = Field(
default="gradient", description="gradient, image, solid"
)
class HomepageSections(BaseModel):
"""Complete homepage sections structure."""
hero: Optional[HeroSection] = None
features: Optional[FeaturesSection] = None
pricing: Optional[PricingSection] = None
cta: Optional[CTASection] = None
@classmethod
def get_empty_structure(cls, languages: list[str]) -> "HomepageSections":
"""
Create empty section structure with language placeholders.
Args:
languages: List of language codes from platform.supported_languages
Returns:
HomepageSections with empty translations for all languages
"""
def make_translatable(langs: list[str]) -> TranslatableText:
return TranslatableText(translations={lang: "" for lang in langs})
return cls(
hero=HeroSection(
title=make_translatable(languages),
subtitle=make_translatable(languages),
buttons=[],
),
features=FeaturesSection(
title=make_translatable(languages),
features=[],
),
pricing=PricingSection(
title=make_translatable(languages),
use_subscription_tiers=True,
),
cta=CTASection(
title=make_translatable(languages),
buttons=[],
),
)
# =============================================================================
# API Request/Response Schemas
# =============================================================================
class SectionUpdateRequest(BaseModel):
"""Request to update a single section."""
section_name: str = Field(..., description="hero, features, pricing, or cta")
section_data: dict = Field(..., description="Section configuration")
class HomepageSectionsResponse(BaseModel):
"""Response containing all homepage sections with platform language info."""
sections: Optional[HomepageSections] = None
supported_languages: list[str] = Field(default_factory=lambda: ["fr", "de", "en"])
default_language: str = "fr"

View File

@@ -899,7 +899,7 @@ class ContentPageService:
ContentPageNotFoundException: If page not found
ValidationError: If sections schema invalid
"""
from models.schema.homepage_sections import HomepageSections
from app.modules.cms.schemas import HomepageSections
page = ContentPageService.get_page_by_id_or_raise(db, page_id)
@@ -941,7 +941,7 @@ class ContentPageService:
ContentPageNotFoundException: If page not found
ValueError: If section name is invalid
"""
from models.schema.homepage_sections import (
from app.modules.cms.schemas import (
HeroSection,
FeaturesSection,
PricingSection,
@@ -989,7 +989,7 @@ class ContentPageService:
Returns:
Empty sections dict with language placeholders
"""
from models.schema.homepage_sections import HomepageSections
from app.modules.cms.schemas import HomepageSections
if languages is None:
languages = ["fr", "de", "en"]

View File

@@ -9,9 +9,9 @@ from datetime import UTC, datetime
from sqlalchemy import case, desc, func
from sqlalchemy.orm import Session
from models.database.architecture_scan import ArchitectureScan
from app.modules.dev_tools.models import ArchitectureScan
from app.modules.marketplace.models import MarketplaceImportJob
from models.database.test_run import TestRun
from app.modules.dev_tools.models import TestRun
class BackgroundTasksService:

View File

@@ -172,7 +172,7 @@ async def homepage(
context["platform"] = platform
# Include subscription tiers for pricing section
from models.database.subscription import TIER_LIMITS, TierCode
from app.modules.billing.models import TIER_LIMITS, TierCode
tiers = []
for tier_code, limits in TIER_LIMITS.items():
@@ -202,7 +202,7 @@ async def homepage(
context["platform"] = platform
# Fetch tiers for display (use API service internally)
from models.database.subscription import TIER_LIMITS, TierCode
from app.modules.billing.models import TIER_LIMITS, TierCode
tiers = []
for tier_code, limits in TIER_LIMITS.items():
@@ -276,7 +276,7 @@ async def pricing_page(
context = get_platform_context(request, db)
# Reuse tier data from homepage
from models.database.subscription import TIER_LIMITS, TierCode
from app.modules.billing.models import TIER_LIMITS, TierCode
tiers = []
for tier_code, limits in TIER_LIMITS.items():
@@ -352,7 +352,7 @@ async def signup_page(
context["is_annual"] = annual
# Get tiers for tier selection step
from models.database.subscription import TIER_LIMITS, TierCode
from app.modules.billing.models import TIER_LIMITS, TierCode
tiers = []
for tier_code, limits in TIER_LIMITS.items():

View File

@@ -17,7 +17,7 @@ from sqlalchemy import func
from sqlalchemy.orm import Session
from app.modules.catalog.models import Product
from models.database.subscription import (
from app.modules.billing.models import (
CapacitySnapshot,
SubscriptionStatus,
VendorSubscription,

View File

@@ -35,7 +35,7 @@ from app.exceptions.feature import (
TierNotFoundError,
)
from models.database.feature import Feature, FeatureCode
from models.database.subscription import SubscriptionTier, VendorSubscription
from app.modules.billing.models import SubscriptionTier, VendorSubscription
logger = logging.getLogger(__name__)

View File

@@ -172,7 +172,7 @@ class PlatformHealthService:
Returns aggregated limits and current usage for capacity planning.
"""
from models.database.subscription import VendorSubscription
from app.modules.billing.models import VendorSubscription
from models.database.vendor import VendorUser
# Get all active subscriptions with their limits

View File

@@ -7,7 +7,7 @@ Handles database operations for subscription tiers and add-on products.
from sqlalchemy.orm import Session
from models.database.subscription import (
from app.modules.billing.models import (
AddOnProduct,
SubscriptionTier,
TIER_LIMITS,

View File

@@ -27,7 +27,7 @@ from app.services.onboarding_service import OnboardingService
from app.services.stripe_service import stripe_service
from middleware.auth import AuthManager
from models.database.company import Company
from models.database.subscription import (
from app.modules.billing.models import (
SubscriptionStatus,
TierCode,
TIER_LIMITS,

View File

@@ -8,7 +8,7 @@ from datetime import UTC, datetime
from app.core.database import SessionLocal
from app.services.admin_notification_service import admin_notification_service
from models.database.architecture_scan import ArchitectureScan, ArchitectureViolation
from app.modules.dev_tools.models import ArchitectureScan, ArchitectureViolation
logger = logging.getLogger(__name__)

View File

@@ -14,7 +14,7 @@ from datetime import UTC, datetime, timedelta
from app.core.database import SessionLocal
from app.services.stripe_service import stripe_service
from models.database.subscription import SubscriptionStatus, VendorSubscription
from app.modules.billing.models import SubscriptionStatus, VendorSubscription
logger = logging.getLogger(__name__)

View File

@@ -5,7 +5,7 @@ import logging
from app.core.database import SessionLocal
from app.services.test_runner_service import test_runner_service
from models.database.test_run import TestRun
from app.modules.dev_tools.models import TestRun
logger = logging.getLogger(__name__)