refactor: convert legacy models/schemas to re-exports

Legacy model and schema files now re-export from module locations
for backwards compatibility:

models/database/:
- letzshop.py -> app.modules.marketplace.models
- marketplace_import_job.py -> app.modules.marketplace.models
- marketplace_product.py -> app.modules.marketplace.models
- marketplace_product_translation.py -> app.modules.marketplace.models
- subscription.py -> app.modules.billing.models
- architecture_scan.py -> app.modules.dev_tools.models
- test_run.py -> app.modules.dev_tools.models

models/schema/:
- marketplace_import_job.py -> app.modules.marketplace.schemas
- marketplace_product.py -> app.modules.marketplace.schemas
- subscription.py -> app.modules.billing.schemas
- stats.py -> app.modules.analytics.schemas

This maintains import compatibility while moving actual code
to self-contained modules.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-28 22:22:18 +01:00
parent 37cf74cbf4
commit 3ffa337fca
11 changed files with 350 additions and 2836 deletions

View File

@@ -1,167 +1,39 @@
from datetime import datetime
# models/schema/marketplace_import_job.py
"""
Legacy location for marketplace import job schemas.
from pydantic import BaseModel, ConfigDict, Field, field_validator
MIGRATED: All schemas have been moved to app.modules.marketplace.schemas.marketplace_import_job.
class MarketplaceImportJobRequest(BaseModel):
"""Request schema for triggering marketplace import.
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: int | None = Field(
1000, description="Processing batch size", ge=100, le=10000
)
language: str = Field(
default="en",
description="Language code for product translations (e.g., 'en', 'fr', 'de')",
New location:
from app.modules.marketplace.schemas import (
MarketplaceImportJobRequest,
MarketplaceImportJobResponse,
)
@field_validator("source_url")
@classmethod
def validate_url(cls, v):
if not v.startswith(("http://", "https://")): # noqa: SEC-034
raise ValueError("URL must start with http:// or https://") # noqa: SEC-034
return v.strip()
This file re-exports from the new location for backward compatibility.
"""
@field_validator("marketplace")
@classmethod
def validate_marketplace(cls, v):
return v.strip()
# Re-export everything from the new canonical location
from app.modules.marketplace.schemas.marketplace_import_job import (
MarketplaceImportJobRequest,
AdminMarketplaceImportJobRequest,
MarketplaceImportJobResponse,
MarketplaceImportJobListResponse,
MarketplaceImportErrorResponse,
MarketplaceImportErrorListResponse,
AdminMarketplaceImportJobResponse,
AdminMarketplaceImportJobListResponse,
MarketplaceImportJobStatusUpdate,
)
@field_validator("language")
@classmethod
def validate_language(cls, v):
# Basic language code validation (2-5 chars)
v = v.strip().lower()
if not 2 <= len(v) <= 5:
raise ValueError("Language code must be 2-5 characters (e.g., 'en', 'fr')")
return v
class AdminMarketplaceImportJobRequest(BaseModel):
"""Request schema for admin-triggered marketplace import.
Includes vendor_id since admin can import for any vendor.
"""
vendor_id: int = Field(..., description="Vendor ID to import products for")
source_url: str = Field(..., description="URL to CSV file from marketplace")
marketplace: str = Field(default="Letzshop", description="Marketplace name")
batch_size: int | None = Field(
1000, description="Processing batch size", ge=100, le=10000
)
language: str = Field(
default="en",
description="Language code for product translations (e.g., 'en', 'fr', 'de')",
)
@field_validator("source_url")
@classmethod
def validate_url(cls, v):
if not v.startswith(("http://", "https://")): # noqa: SEC-034
raise ValueError("URL must start with http:// or https://") # noqa: SEC-034
return v.strip()
@field_validator("marketplace")
@classmethod
def validate_marketplace(cls, v):
return v.strip()
@field_validator("language")
@classmethod
def validate_language(cls, v):
v = v.strip().lower()
if not 2 <= len(v) <= 5:
raise ValueError("Language code must be 2-5 characters (e.g., 'en', 'fr')")
return v
class MarketplaceImportErrorResponse(BaseModel):
"""Response schema for individual import error."""
model_config = ConfigDict(from_attributes=True)
id: int
row_number: int
identifier: str | None = None
error_type: str
error_message: str
row_data: dict | None = None
created_at: datetime
class MarketplaceImportErrorListResponse(BaseModel):
"""Response schema for list of import errors."""
errors: list[MarketplaceImportErrorResponse]
total: int
import_job_id: int
class MarketplaceImportJobResponse(BaseModel):
"""Response schema for marketplace import job."""
model_config = ConfigDict(from_attributes=True)
job_id: int
vendor_id: int
vendor_code: str | None = None # Populated from vendor relationship
vendor_name: str | None = None # Populated from vendor relationship
marketplace: str
source_url: str
status: str
language: str | None = None # Language used for translations
# Counts
imported: int = 0
updated: int = 0
total_processed: int = 0
error_count: int = 0
# Error details
error_message: str | None = None
# Timestamps
created_at: datetime
started_at: datetime | None = None
completed_at: datetime | None = None
class MarketplaceImportJobListResponse(BaseModel):
"""Response schema for list of import jobs."""
jobs: list[MarketplaceImportJobResponse]
total: int
skip: int
limit: int
class AdminMarketplaceImportJobResponse(MarketplaceImportJobResponse):
"""Extended response schema for admin with additional fields."""
id: int # Alias for job_id (frontend compatibility)
error_details: list = [] # Placeholder for future error details
created_by_name: str | None = None # Username of who created the job
class AdminMarketplaceImportJobListResponse(BaseModel):
"""Response schema for paginated list of import jobs (admin)."""
items: list[AdminMarketplaceImportJobResponse]
total: int
page: int
limit: int
class MarketplaceImportJobStatusUpdate(BaseModel):
"""Schema for updating import job status (internal use)."""
status: str
imported_count: int | None = None
updated_count: int | None = None
error_count: int | None = None
total_processed: int | None = None
error_message: str | None = None
__all__ = [
"MarketplaceImportJobRequest",
"AdminMarketplaceImportJobRequest",
"MarketplaceImportJobResponse",
"MarketplaceImportJobListResponse",
"MarketplaceImportErrorResponse",
"MarketplaceImportErrorListResponse",
"AdminMarketplaceImportJobResponse",
"AdminMarketplaceImportJobListResponse",
"MarketplaceImportJobStatusUpdate",
]

View File

@@ -1,225 +1,45 @@
# models/schema/marketplace_product.py
"""Pydantic schemas for MarketplaceProduct API validation.
"""
Legacy location for marketplace product schemas.
Note: title and description are stored in MarketplaceProductTranslation table,
but we keep them in the API schemas for convenience. The service layer
handles creating/updating translations separately.
MIGRATED: All schemas have been moved to app.modules.marketplace.schemas.marketplace_product.
New location:
from app.modules.marketplace.schemas import (
MarketplaceProductCreate,
MarketplaceProductResponse,
MarketplaceProductTranslationSchema,
)
This file re-exports from the new location for backward compatibility.
"""
from datetime import datetime
# Re-export everything from the new canonical location
from app.modules.marketplace.schemas.marketplace_product import (
# Translation schemas
MarketplaceProductTranslationSchema,
# Base schemas
MarketplaceProductBase,
# CRUD schemas
MarketplaceProductCreate,
MarketplaceProductUpdate,
# Response schemas
MarketplaceProductResponse,
MarketplaceProductListResponse,
MarketplaceProductDetailResponse,
# Import schemas
MarketplaceImportRequest,
MarketplaceImportResponse,
)
from pydantic import BaseModel, ConfigDict, Field
from models.schema.inventory import ProductInventorySummary
class MarketplaceProductTranslationSchema(BaseModel):
"""Schema for product translation."""
model_config = ConfigDict(from_attributes=True)
language: str
title: str
description: str | None = None
short_description: str | None = None
meta_title: str | None = None
meta_description: str | None = None
url_slug: str | None = None
class MarketplaceProductBase(BaseModel):
"""Base schema for marketplace products."""
marketplace_product_id: str | None = None
# Localized fields (passed to translations)
title: str | None = None
description: str | None = None
# Links and media
link: str | None = None
image_link: str | None = None
additional_image_link: str | None = None
# Status
availability: str | None = None
is_active: bool | None = None
# Pricing
price: str | None = None
sale_price: str | None = None
currency: str | None = None
# Product identifiers
brand: str | None = None
gtin: str | None = None
mpn: str | None = None
sku: str | None = None
# Product attributes
condition: str | None = None
adult: str | None = None
multipack: int | None = None
is_bundle: str | None = None
age_group: str | None = None
color: str | None = None
gender: str | None = None
material: str | None = None
pattern: str | None = None
size: str | None = None
size_type: str | None = None
size_system: str | None = None
item_group_id: str | None = None
# Categories
google_product_category: str | None = None
product_type_raw: str | None = (
None # Original feed value (renamed from product_type)
)
category_path: str | None = None
# Custom labels
custom_label_0: str | None = None
custom_label_1: str | None = None
custom_label_2: str | None = None
custom_label_3: str | None = None
custom_label_4: str | None = None
# Unit pricing
unit_pricing_measure: str | None = None
unit_pricing_base_measure: str | None = None
identifier_exists: str | None = None
shipping: str | None = None
# Source tracking
marketplace: str | None = None
vendor_name: str | None = None
source_url: str | None = None
# Product type classification
product_type_enum: str | None = (
None # 'physical', 'digital', 'service', 'subscription'
)
is_digital: bool | None = None
# Digital product fields
digital_delivery_method: str | None = None
platform: str | None = None
license_type: str | None = None
# Physical product fields
weight: float | None = None
weight_unit: str | None = None
class MarketplaceProductCreate(MarketplaceProductBase):
"""Schema for creating a marketplace product."""
marketplace_product_id: str = Field(
..., description="Unique product identifier from marketplace"
)
# Title is required for API creation (will be stored in translations)
title: str = Field(..., description="Product title")
class MarketplaceProductUpdate(MarketplaceProductBase):
"""Schema for updating a marketplace product.
All fields are optional - only provided fields will be updated.
"""
class MarketplaceProductResponse(BaseModel):
"""Schema for marketplace product API response."""
model_config = ConfigDict(from_attributes=True)
id: int
marketplace_product_id: str
# These will be populated from translations
title: str | None = None
description: str | None = None
# Links and media
link: str | None = None
image_link: str | None = None
additional_image_link: str | None = None
# Status
availability: str | None = None
is_active: bool | None = None
# Pricing
price: str | None = None
price_numeric: float | None = None
sale_price: str | None = None
sale_price_numeric: float | None = None
currency: str | None = None
# Product identifiers
brand: str | None = None
gtin: str | None = None
mpn: str | None = None
sku: str | None = None
# Product attributes
condition: str | None = None
color: str | None = None
size: str | None = None
# Categories
google_product_category: str | None = None
product_type_raw: str | None = None
category_path: str | None = None
# Source tracking
marketplace: str | None = None
vendor_name: str | None = None
# Product type
product_type_enum: str | None = None
is_digital: bool | None = None
platform: str | None = None
# Timestamps
created_at: datetime
updated_at: datetime
# Translations (optional - included when requested)
translations: list[MarketplaceProductTranslationSchema] | None = None
class MarketplaceProductListResponse(BaseModel):
"""Schema for paginated product list response."""
products: list[MarketplaceProductResponse]
total: int
skip: int
limit: int
class MarketplaceProductDetailResponse(BaseModel):
"""Schema for detailed product response with inventory."""
product: MarketplaceProductResponse
inventory_info: ProductInventorySummary | None = None
translations: list[MarketplaceProductTranslationSchema] | None = None
class MarketplaceImportRequest(BaseModel):
"""Schema for marketplace import request."""
url: str = Field(..., description="URL to CSV file")
marketplace: str = Field(default="Letzshop", description="Marketplace name")
vendor_name: str | None = Field(default=None, description="Vendor name")
language: str = Field(default="en", description="Language code for translations")
batch_size: int = Field(default=100, ge=1, le=1000, description="Batch size")
class MarketplaceImportResponse(BaseModel):
"""Schema for marketplace import response."""
job_id: int
status: str
message: str
__all__ = [
"MarketplaceProductTranslationSchema",
"MarketplaceProductBase",
"MarketplaceProductCreate",
"MarketplaceProductUpdate",
"MarketplaceProductResponse",
"MarketplaceProductListResponse",
"MarketplaceProductDetailResponse",
"MarketplaceImportRequest",
"MarketplaceImportResponse",
]

View File

@@ -1,319 +1,63 @@
from datetime import datetime
from decimal import Decimal
from typing import Any
from pydantic import BaseModel, Field
class StatsResponse(BaseModel):
"""Comprehensive platform statistics response schema."""
total_products: int
unique_brands: int
unique_categories: int
unique_marketplaces: int = 0
unique_vendors: int = 0
total_inventory_entries: int = 0
total_inventory_quantity: int = 0
class MarketplaceStatsResponse(BaseModel):
"""Statistics per marketplace response schema."""
marketplace: str
total_products: int
unique_vendors: int
unique_brands: int
# ============================================================================
# Import Statistics
# ============================================================================
class ImportStatsResponse(BaseModel):
"""Import job statistics response schema.
Used by: GET /api/v1/admin/marketplace-import-jobs/stats
"""
total: int = Field(..., description="Total number of import jobs")
pending: int = Field(..., description="Jobs waiting to start")
processing: int = Field(..., description="Jobs currently running")
completed: int = Field(..., description="Successfully completed jobs")
failed: int = Field(..., description="Failed jobs")
success_rate: float = Field(..., description="Percentage of successful imports")
# ============================================================================
# User Statistics
# ============================================================================
class UserStatsResponse(BaseModel):
"""User statistics response schema.
Used by: Platform statistics endpoints
"""
total_users: int = Field(..., description="Total number of users")
active_users: int = Field(..., description="Number of active users")
inactive_users: int = Field(..., description="Number of inactive users")
admin_users: int = Field(..., description="Number of admin users")
activation_rate: float = Field(..., description="Percentage of active users")
# ============================================================================
# Vendor Statistics (Admin)
# ============================================================================
class VendorStatsResponse(BaseModel):
"""Vendor statistics response schema for admin dashboard.
Used by: GET /api/v1/admin/vendors/stats
"""
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")
# ============================================================================
# Product Statistics
# ============================================================================
class ProductStatsResponse(BaseModel):
"""Product statistics response schema.
Used by: Platform statistics endpoints
"""
total_products: int = Field(0, description="Total number of products")
active_products: int = Field(0, description="Number of active products")
out_of_stock: int = Field(0, description="Number of out-of-stock products")
# ============================================================================
# Platform Statistics (Combined)
# ============================================================================
class PlatformStatsResponse(BaseModel):
"""Combined platform statistics response schema.
Used by: GET /api/v1/admin/dashboard/stats/platform
"""
users: UserStatsResponse
vendors: VendorStatsResponse
products: ProductStatsResponse
orders: "OrderStatsBasicResponse"
imports: ImportStatsResponse
class OrderStatsBasicResponse(BaseModel):
"""Basic order statistics (stub until Order model is fully implemented).
Used by: Platform statistics endpoints
"""
total_orders: int = Field(0, description="Total number of orders")
pending_orders: int = Field(0, description="Number of pending orders")
completed_orders: int = Field(0, description="Number of completed orders")
# ============================================================================
# Admin Dashboard Response
# ============================================================================
class AdminDashboardResponse(BaseModel):
"""Admin dashboard response schema.
Used by: GET /api/v1/admin/dashboard
"""
platform: dict[str, Any] = Field(..., description="Platform information")
users: UserStatsResponse
vendors: VendorStatsResponse
recent_vendors: list[dict[str, Any]] = Field(
default_factory=list, description="Recent vendors"
)
recent_imports: list[dict[str, Any]] = Field(
default_factory=list, description="Recent import jobs"
)
# ============================================================================
# Vendor Dashboard Statistics
# ============================================================================
class VendorProductStats(BaseModel):
"""Vendor product statistics."""
total: int = Field(0, description="Total products in catalog")
active: int = Field(0, description="Active products")
class VendorOrderStats(BaseModel):
"""Vendor order statistics."""
total: int = Field(0, description="Total orders")
pending: int = Field(0, description="Pending orders")
completed: int = Field(0, description="Completed orders")
class VendorCustomerStats(BaseModel):
"""Vendor customer statistics."""
total: int = Field(0, description="Total customers")
active: int = Field(0, description="Active customers")
class VendorRevenueStats(BaseModel):
"""Vendor revenue statistics."""
total: float = Field(0, description="Total revenue")
this_month: float = Field(0, description="Revenue this month")
class VendorInfo(BaseModel):
"""Vendor basic info for dashboard."""
id: int
name: str
vendor_code: str
class VendorDashboardStatsResponse(BaseModel):
"""Vendor dashboard statistics response schema.
Used by: GET /api/v1/vendor/dashboard/stats
"""
vendor: VendorInfo
products: VendorProductStats
orders: VendorOrderStats
customers: VendorCustomerStats
revenue: VendorRevenueStats
# ============================================================================
# Vendor Analytics
# ============================================================================
class VendorAnalyticsImports(BaseModel):
"""Vendor import analytics."""
count: int = Field(0, description="Number of imports in period")
class VendorAnalyticsCatalog(BaseModel):
"""Vendor catalog analytics."""
products_added: int = Field(0, description="Products added in period")
class VendorAnalyticsInventory(BaseModel):
"""Vendor inventory analytics."""
total_locations: int = Field(0, description="Total inventory locations")
class VendorAnalyticsResponse(BaseModel):
"""Vendor analytics response schema.
Used by: GET /api/v1/vendor/analytics
"""
period: str = Field(..., description="Analytics period (e.g., '30d')")
start_date: str = Field(..., description="Period start date")
imports: VendorAnalyticsImports
catalog: VendorAnalyticsCatalog
inventory: VendorAnalyticsInventory
# ============================================================================
# Code Quality Dashboard Statistics
# ============================================================================
class ValidatorStats(BaseModel):
"""Statistics for a single validator type."""
total_violations: int = 0
errors: int = 0
warnings: int = 0
last_scan: str | None = None
class CodeQualityDashboardStatsResponse(BaseModel):
"""Code quality dashboard statistics response schema.
Used by: GET /api/v1/admin/code-quality/stats
Supports multiple validator types: architecture, security, performance.
When validator_type is specified, returns stats for that type only.
When not specified, returns combined stats with per-validator breakdown.
"""
total_violations: int
errors: int
warnings: int
info: int = 0
open: int
assigned: int
resolved: int
ignored: int
technical_debt_score: int
trend: list[dict[str, Any]] = Field(default_factory=list)
by_severity: dict[str, Any] = Field(default_factory=dict)
by_rule: dict[str, Any] = Field(default_factory=dict)
by_module: dict[str, Any] = Field(default_factory=dict)
top_files: list[dict[str, Any]] = Field(default_factory=list)
last_scan: str | None = None
validator_type: str | None = None # Set when filtering by type
by_validator: dict[str, ValidatorStats] = Field(
default_factory=dict,
description="Per-validator breakdown (architecture, security, performance)",
)
# ============================================================================
# Customer Statistics (Coming Soon)
# ============================================================================
class CustomerStatsResponse(BaseModel):
"""Schema for customer statistics."""
customer_id: int
total_orders: int
total_spent: Decimal
average_order_value: Decimal
last_order_date: datetime | None
first_order_date: datetime | None
lifetime_value: Decimal
# ============================================================================
# Order Statistics (Coming Soon)
# ============================================================================
class OrderStatsResponse(BaseModel):
"""Schema for order statistics."""
total_orders: int
pending_orders: int
processing_orders: int
shipped_orders: int
delivered_orders: int
cancelled_orders: int
total_revenue: Decimal
average_order_value: Decimal
# models/schema/stats.py
"""
Statistics schemas - LEGACY LOCATION
This file exists for backward compatibility.
The canonical location is now: app/modules/analytics/schemas/stats.py
All imports should use the new location:
from app.modules.analytics.schemas import StatsResponse, ...
"""
# Re-export from canonical location for backward compatibility
from app.modules.analytics.schemas.stats import (
StatsResponse,
MarketplaceStatsResponse,
ImportStatsResponse,
UserStatsResponse,
VendorStatsResponse,
ProductStatsResponse,
PlatformStatsResponse,
OrderStatsBasicResponse,
AdminDashboardResponse,
VendorProductStats,
VendorOrderStats,
VendorCustomerStats,
VendorRevenueStats,
VendorInfo,
VendorDashboardStatsResponse,
VendorAnalyticsImports,
VendorAnalyticsCatalog,
VendorAnalyticsInventory,
VendorAnalyticsResponse,
ValidatorStats,
CodeQualityDashboardStatsResponse,
CustomerStatsResponse,
OrderStatsResponse,
)
__all__ = [
"StatsResponse",
"MarketplaceStatsResponse",
"ImportStatsResponse",
"UserStatsResponse",
"VendorStatsResponse",
"ProductStatsResponse",
"PlatformStatsResponse",
"OrderStatsBasicResponse",
"AdminDashboardResponse",
"VendorProductStats",
"VendorOrderStats",
"VendorCustomerStats",
"VendorRevenueStats",
"VendorInfo",
"VendorDashboardStatsResponse",
"VendorAnalyticsImports",
"VendorAnalyticsCatalog",
"VendorAnalyticsInventory",
"VendorAnalyticsResponse",
"ValidatorStats",
"CodeQualityDashboardStatsResponse",
"CustomerStatsResponse",
"OrderStatsResponse",
]

View File

@@ -1,209 +1,58 @@
# models/schema/subscription.py
"""
Pydantic schemas for subscription operations.
Legacy location for subscription schemas.
Supports subscription management and tier limit checks.
MIGRATED: All schemas have been moved to app.modules.billing.schemas.subscription.
New location:
from app.modules.billing.schemas import (
SubscriptionCreate,
SubscriptionResponse,
TierInfo,
)
This file re-exports from the new location for backward compatibility.
"""
from datetime import datetime
# Re-export everything from the new canonical location
from app.modules.billing.schemas.subscription import (
# Tier schemas
TierFeatures,
TierLimits,
TierInfo,
# Subscription CRUD schemas
SubscriptionCreate,
SubscriptionUpdate,
SubscriptionResponse,
# Usage schemas
SubscriptionUsage,
UsageSummary,
SubscriptionStatusResponse,
# Limit check schemas
LimitCheckResult,
CanCreateOrderResponse,
CanAddProductResponse,
CanAddTeamMemberResponse,
FeatureCheckResponse,
)
from pydantic import BaseModel, ConfigDict, Field
# ============================================================================
# Tier Information Schemas
# ============================================================================
class TierFeatures(BaseModel):
"""Features included in a tier."""
letzshop_sync: bool = True
inventory_basic: bool = True
inventory_locations: bool = False
inventory_purchase_orders: bool = False
invoice_lu: bool = True
invoice_eu_vat: bool = False
invoice_bulk: bool = False
customer_view: bool = True
customer_export: bool = False
analytics_dashboard: bool = False
accounting_export: bool = False
api_access: bool = False
automation_rules: bool = False
team_roles: bool = False
white_label: bool = False
multi_vendor: bool = False
custom_integrations: bool = False
sla_guarantee: bool = False
dedicated_support: bool = False
class TierLimits(BaseModel):
"""Limits for a subscription tier."""
orders_per_month: int | None = Field(None, description="None = unlimited")
products_limit: int | None = Field(None, description="None = unlimited")
team_members: int | None = Field(None, description="None = unlimited")
order_history_months: int | None = Field(None, description="None = unlimited")
class TierInfo(BaseModel):
"""Full tier information."""
code: str
name: str
price_monthly_cents: int
price_annual_cents: int | None
limits: TierLimits
features: list[str]
# ============================================================================
# Subscription Schemas
# ============================================================================
class SubscriptionCreate(BaseModel):
"""Schema for creating a subscription (admin/internal use)."""
tier: str = Field(default="essential", pattern="^(essential|professional|business|enterprise)$")
is_annual: bool = False
trial_days: int = Field(default=14, ge=0, le=30)
class SubscriptionUpdate(BaseModel):
"""Schema for updating a subscription."""
tier: str | None = Field(None, pattern="^(essential|professional|business|enterprise)$")
status: str | None = Field(None, pattern="^(trial|active|past_due|cancelled|expired)$")
is_annual: bool | None = None
custom_orders_limit: int | None = None
custom_products_limit: int | None = None
custom_team_limit: int | None = None
class SubscriptionResponse(BaseModel):
"""Schema for subscription response."""
model_config = ConfigDict(from_attributes=True)
id: int
vendor_id: int
tier: str
status: str
period_start: datetime
period_end: datetime
is_annual: bool
trial_ends_at: datetime | None
orders_this_period: int
orders_limit_reached_at: datetime | None
# Effective limits (with custom overrides applied)
orders_limit: int | None
products_limit: int | None
team_members_limit: int | None
# Computed properties
is_active: bool
is_trial: bool
trial_days_remaining: int | None
created_at: datetime
updated_at: datetime
class SubscriptionUsage(BaseModel):
"""Current subscription usage statistics."""
orders_used: int
orders_limit: int | None
orders_remaining: int | None
orders_percent_used: float | None
products_used: int
products_limit: int | None
products_remaining: int | None
products_percent_used: float | None
team_members_used: int
team_members_limit: int | None
team_members_remaining: int | None
team_members_percent_used: float | None
class UsageSummary(BaseModel):
"""Usage summary for billing page display."""
orders_this_period: int
orders_limit: int | None
orders_remaining: int | None
products_count: int
products_limit: int | None
products_remaining: int | None
team_count: int
team_limit: int | None
team_remaining: int | None
class SubscriptionStatusResponse(BaseModel):
"""Subscription status with usage and limits."""
subscription: SubscriptionResponse
usage: SubscriptionUsage
tier_info: TierInfo
# ============================================================================
# Limit Check Schemas
# ============================================================================
class LimitCheckResult(BaseModel):
"""Result of a limit check."""
allowed: bool
limit: int | None
current: int
remaining: int | None
message: str | None = None
class CanCreateOrderResponse(BaseModel):
"""Response for order creation check."""
allowed: bool
orders_this_period: int
orders_limit: int | None
message: str | None = None
class CanAddProductResponse(BaseModel):
"""Response for product addition check."""
allowed: bool
products_count: int
products_limit: int | None
message: str | None = None
class CanAddTeamMemberResponse(BaseModel):
"""Response for team member addition check."""
allowed: bool
team_count: int
team_limit: int | None
message: str | None = None
class FeatureCheckResponse(BaseModel):
"""Response for feature check."""
feature: str
enabled: bool
tier_required: str | None = None
message: str | None = None
__all__ = [
# Tier schemas
"TierFeatures",
"TierLimits",
"TierInfo",
# Subscription CRUD schemas
"SubscriptionCreate",
"SubscriptionUpdate",
"SubscriptionResponse",
# Usage schemas
"SubscriptionUsage",
"UsageSummary",
"SubscriptionStatusResponse",
# Limit check schemas
"LimitCheckResult",
"CanCreateOrderResponse",
"CanAddProductResponse",
"CanAddTeamMemberResponse",
"FeatureCheckResponse",
]