fix(lint): auto-fix ruff violations and tune lint rules
- Auto-fixed 4,496 lint issues (import sorting, modern syntax, etc.) - Added ignore rules for patterns intentional in this codebase: E402 (late imports), E712 (SQLAlchemy filters), B904 (raise from), SIM108/SIM105/SIM117 (readability preferences) - Added per-file ignores for tests and scripts - Excluded broken scripts/rename_terminology.py (has curly quotes) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -54,31 +54,31 @@ Usage:
|
||||
"""
|
||||
|
||||
from app.modules.base import ModuleDefinition, ScheduledTask
|
||||
from app.modules.task_base import ModuleTask, DatabaseTask
|
||||
from app.modules.tasks import (
|
||||
discover_module_tasks,
|
||||
build_beat_schedule,
|
||||
parse_schedule,
|
||||
get_module_task_routes,
|
||||
from app.modules.events import (
|
||||
ModuleEvent,
|
||||
ModuleEventBus,
|
||||
ModuleEventData,
|
||||
module_event_bus,
|
||||
)
|
||||
from app.modules.registry import (
|
||||
MODULES,
|
||||
CORE_MODULES,
|
||||
OPTIONAL_MODULES,
|
||||
INTERNAL_MODULES,
|
||||
MODULES,
|
||||
OPTIONAL_MODULES,
|
||||
get_core_module_codes,
|
||||
get_optional_module_codes,
|
||||
get_internal_module_codes,
|
||||
get_module_tier,
|
||||
get_optional_module_codes,
|
||||
is_core_module,
|
||||
is_internal_module,
|
||||
)
|
||||
from app.modules.service import ModuleService, module_service
|
||||
from app.modules.events import (
|
||||
ModuleEvent,
|
||||
ModuleEventData,
|
||||
ModuleEventBus,
|
||||
module_event_bus,
|
||||
from app.modules.task_base import DatabaseTask, ModuleTask
|
||||
from app.modules.tasks import (
|
||||
build_beat_schedule,
|
||||
discover_module_tasks,
|
||||
get_module_task_routes,
|
||||
parse_schedule,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -25,7 +25,7 @@ def __getattr__(name: str):
|
||||
from app.modules.analytics.definition import analytics_module
|
||||
|
||||
return analytics_module
|
||||
elif name == "get_analytics_module_with_routers":
|
||||
if name == "get_analytics_module_with_routers":
|
||||
from app.modules.analytics.definition import get_analytics_module_with_routers
|
||||
|
||||
return get_analytics_module_with_routers
|
||||
|
||||
@@ -6,7 +6,12 @@ Defines the analytics module including its features, menu items,
|
||||
route configurations, and self-contained module settings.
|
||||
"""
|
||||
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition
|
||||
from app.modules.base import (
|
||||
MenuItemDefinition,
|
||||
MenuSectionDefinition,
|
||||
ModuleDefinition,
|
||||
PermissionDefinition,
|
||||
)
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
|
||||
@@ -26,7 +31,9 @@ def _get_store_page_router():
|
||||
|
||||
def _get_feature_provider():
|
||||
"""Lazy import of feature provider to avoid circular imports."""
|
||||
from app.modules.analytics.services.analytics_features import analytics_feature_provider
|
||||
from app.modules.analytics.services.analytics_features import (
|
||||
analytics_feature_provider,
|
||||
)
|
||||
|
||||
return analytics_feature_provider
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ def __getattr__(name: str):
|
||||
if name == "store_api_router":
|
||||
from app.modules.analytics.routes.api import store_router
|
||||
return store_router
|
||||
elif name == "store_page_router":
|
||||
if name == "store_page_router":
|
||||
from app.modules.analytics.routes.pages import store_router
|
||||
return store_router
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
@@ -16,14 +16,14 @@ from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_store_api, get_db, require_module_access
|
||||
from app.modules.billing.dependencies.feature_gate import RequireFeature
|
||||
from app.modules.analytics.services import stats_service
|
||||
from app.modules.analytics.schemas import (
|
||||
StoreAnalyticsCatalog,
|
||||
StoreAnalyticsImports,
|
||||
StoreAnalyticsInventory,
|
||||
StoreAnalyticsResponse,
|
||||
)
|
||||
from app.modules.analytics.services import stats_service
|
||||
from app.modules.billing.dependencies.feature_gate import RequireFeature
|
||||
from app.modules.enums import FrontendType
|
||||
from app.modules.tenancy.models import User
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db, require_menu_access
|
||||
from app.modules.core.utils.page_context import get_admin_context
|
||||
from app.templates_config import templates
|
||||
from app.modules.enums import FrontendType
|
||||
from app.modules.tenancy.models import User
|
||||
from app.templates_config import templates
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -12,10 +12,11 @@ from fastapi.responses import HTMLResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_store_from_cookie_or_header, get_db
|
||||
from app.modules.core.services.platform_settings_service import platform_settings_service # noqa: MOD-004 - shared platform service
|
||||
from app.modules.core.services.platform_settings_service import (
|
||||
platform_settings_service, # noqa: MOD-004 - shared platform service
|
||||
)
|
||||
from app.modules.tenancy.models import Store, User
|
||||
from app.templates_config import templates
|
||||
from app.modules.tenancy.models import User
|
||||
from app.modules.tenancy.models import Store
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -6,29 +6,29 @@ This is the canonical location for analytics schemas.
|
||||
"""
|
||||
|
||||
from app.modules.analytics.schemas.stats import (
|
||||
StatsResponse,
|
||||
MarketplaceStatsResponse,
|
||||
ImportStatsResponse,
|
||||
UserStatsResponse,
|
||||
StoreStatsResponse,
|
||||
ProductStatsResponse,
|
||||
PlatformStatsResponse,
|
||||
OrderStatsBasicResponse,
|
||||
AdminDashboardResponse,
|
||||
StoreProductStats,
|
||||
StoreOrderStats,
|
||||
StoreCustomerStats,
|
||||
StoreRevenueStats,
|
||||
StoreInfo,
|
||||
StoreDashboardStatsResponse,
|
||||
StoreAnalyticsImports,
|
||||
StoreAnalyticsCatalog,
|
||||
StoreAnalyticsInventory,
|
||||
StoreAnalyticsResponse,
|
||||
ValidatorStats,
|
||||
CodeQualityDashboardStatsResponse,
|
||||
CustomerStatsResponse,
|
||||
ImportStatsResponse,
|
||||
MarketplaceStatsResponse,
|
||||
OrderStatsBasicResponse,
|
||||
OrderStatsResponse,
|
||||
PlatformStatsResponse,
|
||||
ProductStatsResponse,
|
||||
StatsResponse,
|
||||
StoreAnalyticsCatalog,
|
||||
StoreAnalyticsImports,
|
||||
StoreAnalyticsInventory,
|
||||
StoreAnalyticsResponse,
|
||||
StoreCustomerStats,
|
||||
StoreDashboardStatsResponse,
|
||||
StoreInfo,
|
||||
StoreOrderStats,
|
||||
StoreProductStats,
|
||||
StoreRevenueStats,
|
||||
StoreStatsResponse,
|
||||
UserStatsResponse,
|
||||
ValidatorStats,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -23,7 +23,6 @@ from app.modules.core.schemas.dashboard import (
|
||||
PlatformStatsResponse,
|
||||
ProductStatsResponse,
|
||||
StatsResponse,
|
||||
UserStatsResponse,
|
||||
StoreCustomerStats,
|
||||
StoreDashboardStatsResponse,
|
||||
StoreInfo,
|
||||
@@ -31,9 +30,9 @@ from app.modules.core.schemas.dashboard import (
|
||||
StoreProductStats,
|
||||
StoreRevenueStats,
|
||||
StoreStatsResponse,
|
||||
UserStatsResponse,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Store Analytics (Analytics-specific, not in core)
|
||||
# ============================================================================
|
||||
|
||||
@@ -6,8 +6,8 @@ This is the canonical location for analytics services.
|
||||
"""
|
||||
|
||||
from app.modules.analytics.services.stats_service import (
|
||||
stats_service,
|
||||
StatsService,
|
||||
stats_service,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -12,11 +12,8 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import func
|
||||
|
||||
from app.modules.contracts.features import (
|
||||
FeatureDeclaration,
|
||||
FeatureProviderProtocol,
|
||||
FeatureScope,
|
||||
FeatureType,
|
||||
FeatureUsage,
|
||||
|
||||
@@ -18,14 +18,16 @@ from typing import Any
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.tenancy.exceptions import AdminOperationException, StoreNotFoundException
|
||||
from app.modules.catalog.models import Product
|
||||
from app.modules.customers.models.customer import Customer
|
||||
from app.modules.inventory.models import Inventory
|
||||
from app.modules.marketplace.models import MarketplaceImportJob, MarketplaceProduct
|
||||
from app.modules.orders.models import Order
|
||||
from app.modules.catalog.models import Product
|
||||
from app.modules.tenancy.models import User
|
||||
from app.modules.tenancy.models import Store
|
||||
from app.modules.tenancy.exceptions import (
|
||||
AdminOperationException,
|
||||
StoreNotFoundException,
|
||||
)
|
||||
from app.modules.tenancy.models import Store, User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -36,9 +36,10 @@ Self-Contained Module Structure:
|
||||
└── locales/ # Translation files
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastapi import APIRouter
|
||||
@@ -52,7 +53,6 @@ if TYPE_CHECKING:
|
||||
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Menu Item Definitions
|
||||
# =============================================================================
|
||||
@@ -805,10 +805,9 @@ class ModuleDefinition:
|
||||
"""
|
||||
if self.is_core:
|
||||
return "core"
|
||||
elif self.is_internal:
|
||||
if self.is_internal:
|
||||
return "internal"
|
||||
else:
|
||||
return "optional"
|
||||
return "optional"
|
||||
|
||||
# =========================================================================
|
||||
# Context Provider Methods
|
||||
|
||||
@@ -39,7 +39,7 @@ def __getattr__(name: str):
|
||||
if name == "billing_module":
|
||||
from app.modules.billing.definition import billing_module
|
||||
return billing_module
|
||||
elif name == "get_billing_module_with_routers":
|
||||
if name == "get_billing_module_with_routers":
|
||||
from app.modules.billing.definition import get_billing_module_with_routers
|
||||
return get_billing_module_with_routers
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
@@ -9,7 +9,13 @@ route configurations, and scheduled tasks.
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition, ScheduledTask
|
||||
from app.modules.base import (
|
||||
MenuItemDefinition,
|
||||
MenuSectionDefinition,
|
||||
ModuleDefinition,
|
||||
PermissionDefinition,
|
||||
ScheduledTask,
|
||||
)
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"""FastAPI dependencies for the billing module."""
|
||||
|
||||
from .feature_gate import (
|
||||
require_feature,
|
||||
RequireFeature,
|
||||
FeatureNotAvailableError,
|
||||
RequireFeature,
|
||||
require_feature,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -37,7 +37,7 @@ Usage:
|
||||
import asyncio
|
||||
import functools
|
||||
import logging
|
||||
from typing import Callable
|
||||
from collections.abc import Callable
|
||||
|
||||
from fastapi import Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -106,7 +106,7 @@ class RequireFeature:
|
||||
|
||||
for feature_code in self.feature_codes:
|
||||
if feature_service.has_feature_for_store(db, store_id, feature_code):
|
||||
return None
|
||||
return
|
||||
|
||||
# None of the features are available
|
||||
feature_code = self.feature_codes[0]
|
||||
@@ -204,8 +204,7 @@ def require_feature(*feature_codes: str) -> Callable:
|
||||
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return async_wrapper
|
||||
else:
|
||||
return sync_wrapper
|
||||
return sync_wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ Revision ID: billing_001
|
||||
Revises: core_001
|
||||
Create Date: 2026-02-07
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision = "billing_001"
|
||||
down_revision = "core_001"
|
||||
branch_labels = None
|
||||
|
||||
@@ -14,7 +14,6 @@ Feature limits per tier are in tier_feature_limit.py.
|
||||
"""
|
||||
|
||||
import enum
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
|
||||
@@ -17,8 +17,6 @@ from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_current_admin_api, require_module_access
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import ResourceNotFoundException
|
||||
from app.modules.billing.services import admin_subscription_service, subscription_service
|
||||
from app.modules.enums import FrontendType
|
||||
from app.modules.billing.schemas import (
|
||||
BillingHistoryListResponse,
|
||||
BillingHistoryWithMerchant,
|
||||
@@ -33,6 +31,11 @@ from app.modules.billing.schemas import (
|
||||
SubscriptionTierResponse,
|
||||
SubscriptionTierUpdate,
|
||||
)
|
||||
from app.modules.billing.services import (
|
||||
admin_subscription_service,
|
||||
subscription_service,
|
||||
)
|
||||
from app.modules.enums import FrontendType
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -17,16 +17,19 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api, require_module_access
|
||||
from app.core.database import get_db
|
||||
from app.modules.billing.services.feature_aggregator import feature_aggregator
|
||||
from app.modules.billing.models.tier_feature_limit import TierFeatureLimit, MerchantFeatureOverride
|
||||
from app.modules.billing.models import SubscriptionTier
|
||||
from app.modules.billing.models.tier_feature_limit import (
|
||||
MerchantFeatureOverride,
|
||||
TierFeatureLimit,
|
||||
)
|
||||
from app.modules.billing.schemas import (
|
||||
FeatureDeclarationResponse,
|
||||
FeatureCatalogResponse,
|
||||
TierFeatureLimitEntry,
|
||||
FeatureDeclarationResponse,
|
||||
MerchantFeatureOverrideEntry,
|
||||
MerchantFeatureOverrideResponse,
|
||||
TierFeatureLimitEntry,
|
||||
)
|
||||
from app.modules.billing.services.feature_aggregator import feature_aggregator
|
||||
from app.modules.enums import FrontendType
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ Provides subscription management and billing operations for merchant owners:
|
||||
- Stripe checkout session creation
|
||||
- Invoice history
|
||||
|
||||
Authentication: merchant_token cookie or Authorization header.
|
||||
Authentication: Authorization header (API-only, no cookies for CSRF safety).
|
||||
The user must own at least one active merchant (validated by
|
||||
get_current_merchant_from_cookie_or_header).
|
||||
get_merchant_for_current_user).
|
||||
|
||||
Auto-discovered by the route system (merchant.py in routes/api/ triggers
|
||||
registration under /api/v1/merchants/billing/*).
|
||||
@@ -18,22 +18,26 @@ registration under /api/v1/merchants/billing/*).
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
|
||||
from pydantic import BaseModel
|
||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_merchant_from_cookie_or_header
|
||||
from app.api.deps import get_merchant_for_current_user
|
||||
from app.core.database import get_db
|
||||
from app.modules.billing.schemas import (
|
||||
ChangeTierRequest,
|
||||
ChangeTierResponse,
|
||||
CheckoutRequest,
|
||||
CheckoutResponse,
|
||||
MerchantPortalAvailableTiersResponse,
|
||||
MerchantPortalInvoiceListResponse,
|
||||
MerchantPortalSubscriptionDetailResponse,
|
||||
MerchantPortalSubscriptionItem,
|
||||
MerchantPortalSubscriptionListResponse,
|
||||
MerchantSubscriptionResponse,
|
||||
TierInfo,
|
||||
)
|
||||
from app.modules.billing.services.billing_service import billing_service
|
||||
from app.modules.billing.services.subscription_service import subscription_service
|
||||
from app.modules.tenancy.models import Merchant
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -44,49 +48,15 @@ ROUTE_CONFIG = {
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Helpers
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def _get_user_merchant(db: Session, user_context: UserContext) -> Merchant:
|
||||
"""
|
||||
Get the first active merchant owned by the current user.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
user_context: Authenticated user context
|
||||
|
||||
Returns:
|
||||
Merchant: The user's active merchant
|
||||
|
||||
Raises:
|
||||
HTTPException 404: If the user has no active merchants
|
||||
"""
|
||||
merchant = (
|
||||
db.query(Merchant)
|
||||
.filter(
|
||||
Merchant.owner_user_id == user_context.id,
|
||||
Merchant.is_active == True, # noqa: E712
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not merchant:
|
||||
raise HTTPException(status_code=404, detail="No active merchant found")
|
||||
|
||||
return merchant
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Subscription Endpoints
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/subscriptions")
|
||||
@router.get("/subscriptions", response_model=MerchantPortalSubscriptionListResponse)
|
||||
def list_merchant_subscriptions(
|
||||
request: Request,
|
||||
current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header),
|
||||
merchant=Depends(get_merchant_for_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -95,7 +65,6 @@ def list_merchant_subscriptions(
|
||||
Returns subscriptions across all platforms the merchant is subscribed to,
|
||||
including tier information and status.
|
||||
"""
|
||||
merchant = _get_user_merchant(db, current_user)
|
||||
subscriptions = subscription_service.get_merchant_subscriptions(db, merchant.id)
|
||||
|
||||
items = []
|
||||
@@ -104,16 +73,21 @@ def list_merchant_subscriptions(
|
||||
data["tier"] = sub.tier.code if sub.tier else None
|
||||
data["tier_name"] = sub.tier.name if sub.tier else None
|
||||
data["platform_name"] = sub.platform.name if sub.platform else ""
|
||||
items.append(data)
|
||||
items.append(MerchantPortalSubscriptionItem(**data))
|
||||
|
||||
return {"subscriptions": items, "total": len(items)}
|
||||
return MerchantPortalSubscriptionListResponse(
|
||||
subscriptions=items, total=len(items)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/subscriptions/{platform_id}")
|
||||
@router.get(
|
||||
"/subscriptions/{platform_id}",
|
||||
response_model=MerchantPortalSubscriptionDetailResponse,
|
||||
)
|
||||
def get_merchant_subscription(
|
||||
request: Request,
|
||||
platform_id: int = Path(..., description="Platform ID"),
|
||||
current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header),
|
||||
merchant=Depends(get_merchant_for_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -121,21 +95,25 @@ def get_merchant_subscription(
|
||||
|
||||
Returns the subscription with tier information for the given platform.
|
||||
"""
|
||||
merchant = _get_user_merchant(db, current_user)
|
||||
subscription = subscription_service.get_merchant_subscription(
|
||||
db, merchant.id, platform_id
|
||||
)
|
||||
|
||||
if not subscription:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No subscription found for platform {platform_id}",
|
||||
from app.exceptions.base import ResourceNotFoundException
|
||||
|
||||
raise ResourceNotFoundException(
|
||||
resource_type="Subscription",
|
||||
identifier=f"merchant={merchant.id}, platform={platform_id}",
|
||||
error_code="SUBSCRIPTION_NOT_FOUND",
|
||||
)
|
||||
|
||||
sub_data = MerchantSubscriptionResponse.model_validate(subscription).model_dump()
|
||||
sub_data["tier"] = subscription.tier.code if subscription.tier else None
|
||||
sub_data["tier_name"] = subscription.tier.name if subscription.tier else None
|
||||
sub_data["platform_name"] = subscription.platform.name if subscription.platform else ""
|
||||
sub_data["platform_name"] = (
|
||||
subscription.platform.name if subscription.platform else ""
|
||||
)
|
||||
|
||||
tier_info = None
|
||||
if subscription.tier:
|
||||
@@ -146,20 +124,25 @@ def get_merchant_subscription(
|
||||
description=tier.description,
|
||||
price_monthly_cents=tier.price_monthly_cents,
|
||||
price_annual_cents=tier.price_annual_cents,
|
||||
feature_codes=tier.get_feature_codes() if hasattr(tier, "get_feature_codes") else [],
|
||||
feature_codes=(
|
||||
tier.get_feature_codes() if hasattr(tier, "get_feature_codes") else []
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
"subscription": sub_data,
|
||||
"tier": tier_info,
|
||||
}
|
||||
return MerchantPortalSubscriptionDetailResponse(
|
||||
subscription=MerchantPortalSubscriptionItem(**sub_data),
|
||||
tier=tier_info,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/subscriptions/{platform_id}/tiers")
|
||||
@router.get(
|
||||
"/subscriptions/{platform_id}/tiers",
|
||||
response_model=MerchantPortalAvailableTiersResponse,
|
||||
)
|
||||
def get_available_tiers(
|
||||
request: Request,
|
||||
platform_id: int = Path(..., description="Platform ID"),
|
||||
current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header),
|
||||
merchant=Depends(get_merchant_for_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -168,7 +151,6 @@ def get_available_tiers(
|
||||
Returns all public tiers with upgrade/downgrade flags relative to
|
||||
the merchant's current tier.
|
||||
"""
|
||||
merchant = _get_user_merchant(db, current_user)
|
||||
subscription = subscription_service.get_merchant_subscription(
|
||||
db, merchant.id, platform_id
|
||||
)
|
||||
@@ -182,25 +164,21 @@ def get_available_tiers(
|
||||
if subscription and subscription.tier:
|
||||
current_tier_code = subscription.tier.code
|
||||
|
||||
return {
|
||||
"tiers": tier_list,
|
||||
"current_tier": current_tier_code,
|
||||
}
|
||||
return MerchantPortalAvailableTiersResponse(
|
||||
tiers=tier_list,
|
||||
current_tier=current_tier_code,
|
||||
)
|
||||
|
||||
|
||||
class ChangeTierRequest(BaseModel):
|
||||
"""Request for changing subscription tier."""
|
||||
|
||||
tier_code: str
|
||||
is_annual: bool = False
|
||||
|
||||
|
||||
@router.post("/subscriptions/{platform_id}/change-tier")
|
||||
@router.post(
|
||||
"/subscriptions/{platform_id}/change-tier",
|
||||
response_model=ChangeTierResponse,
|
||||
)
|
||||
def change_subscription_tier(
|
||||
request: Request,
|
||||
tier_data: ChangeTierRequest,
|
||||
platform_id: int = Path(..., description="Platform ID"),
|
||||
current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header),
|
||||
merchant=Depends(get_merchant_for_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -208,7 +186,6 @@ def change_subscription_tier(
|
||||
|
||||
Handles both Stripe-connected and non-Stripe subscriptions.
|
||||
"""
|
||||
merchant = _get_user_merchant(db, current_user)
|
||||
result = billing_service.change_tier(
|
||||
db, merchant.id, platform_id, tier_data.tier_code, tier_data.is_annual
|
||||
)
|
||||
@@ -230,7 +207,7 @@ def create_checkout_session(
|
||||
request: Request,
|
||||
checkout_data: CheckoutRequest,
|
||||
platform_id: int = Path(..., description="Platform ID"),
|
||||
current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header),
|
||||
merchant=Depends(get_merchant_for_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -239,12 +216,14 @@ def create_checkout_session(
|
||||
Starts a new subscription or upgrades an existing one to the
|
||||
requested tier.
|
||||
"""
|
||||
merchant = _get_user_merchant(db, current_user)
|
||||
|
||||
# Build success/cancel URLs from request
|
||||
base_url = str(request.base_url).rstrip("/")
|
||||
success_url = f"{base_url}/merchants/billing/subscriptions/{platform_id}?checkout=success"
|
||||
cancel_url = f"{base_url}/merchants/billing/subscriptions/{platform_id}?checkout=cancelled"
|
||||
success_url = (
|
||||
f"{base_url}/merchants/billing/subscriptions/{platform_id}?checkout=success"
|
||||
)
|
||||
cancel_url = (
|
||||
f"{base_url}/merchants/billing/subscriptions/{platform_id}?checkout=cancelled"
|
||||
)
|
||||
|
||||
result = billing_service.create_checkout_session(
|
||||
db=db,
|
||||
@@ -274,12 +253,12 @@ def create_checkout_session(
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/invoices")
|
||||
@router.get("/invoices", response_model=MerchantPortalInvoiceListResponse)
|
||||
def get_invoices(
|
||||
request: Request,
|
||||
skip: int = Query(0, ge=0, description="Number of records to skip"),
|
||||
limit: int = Query(20, ge=1, le=100, description="Max records to return"),
|
||||
current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header),
|
||||
merchant=Depends(get_merchant_for_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -287,14 +266,12 @@ def get_invoices(
|
||||
|
||||
Returns paginated billing history entries ordered by date descending.
|
||||
"""
|
||||
merchant = _get_user_merchant(db, current_user)
|
||||
|
||||
invoices, total = billing_service.get_invoices(
|
||||
db, merchant.id, skip=skip, limit=limit
|
||||
)
|
||||
|
||||
return {
|
||||
"invoices": [
|
||||
return MerchantPortalInvoiceListResponse(
|
||||
invoices=[
|
||||
{
|
||||
"id": inv.id,
|
||||
"invoice_number": inv.invoice_number,
|
||||
@@ -309,11 +286,13 @@ def get_invoices(
|
||||
"pdf_url": inv.invoice_pdf_url,
|
||||
"hosted_url": inv.hosted_invoice_url,
|
||||
"description": inv.description,
|
||||
"created_at": inv.created_at.isoformat() if inv.created_at else None,
|
||||
"created_at": (
|
||||
inv.created_at.isoformat() if inv.created_at else None
|
||||
),
|
||||
}
|
||||
for inv in invoices
|
||||
],
|
||||
"total": total,
|
||||
"skip": skip,
|
||||
"limit": limit,
|
||||
}
|
||||
total=total,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
@@ -14,8 +14,10 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import ResourceNotFoundException
|
||||
from app.modules.billing.services.platform_pricing_service import platform_pricing_service
|
||||
from app.modules.billing.models import TierCode, SubscriptionTier
|
||||
from app.modules.billing.models import SubscriptionTier, TierCode
|
||||
from app.modules.billing.services.platform_pricing_service import (
|
||||
platform_pricing_service,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/pricing")
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_store_api, require_module_access
|
||||
from app.core.config import settings
|
||||
from app.core.database import get_db
|
||||
from app.modules.billing.services import billing_service, subscription_service
|
||||
from app.modules.enums import FrontendType
|
||||
@@ -218,9 +217,9 @@ def get_invoices(
|
||||
# ============================================================================
|
||||
# Include all billing-related store sub-routers
|
||||
|
||||
from app.modules.billing.routes.api.store_features import store_features_router
|
||||
from app.modules.billing.routes.api.store_checkout import store_checkout_router
|
||||
from app.modules.billing.routes.api.store_addons import store_addons_router
|
||||
from app.modules.billing.routes.api.store_checkout import store_checkout_router
|
||||
from app.modules.billing.routes.api.store_features import store_features_router
|
||||
from app.modules.billing.routes.api.store_usage import store_usage_router
|
||||
|
||||
store_router.include_router(store_features_router, tags=["store-features"])
|
||||
|
||||
@@ -22,7 +22,7 @@ from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_current_store_api, require_module_access
|
||||
from app.core.config import settings
|
||||
from app.core.database import get_db
|
||||
from app.modules.billing.services import billing_service, subscription_service
|
||||
from app.modules.billing.services import billing_service
|
||||
from app.modules.enums import FrontendType
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db, require_menu_access
|
||||
from app.modules.core.utils.page_context import get_admin_context
|
||||
from app.templates_config import templates
|
||||
from app.modules.enums import FrontendType
|
||||
from app.modules.tenancy.models import User
|
||||
from app.templates_config import templates
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ async def merchant_subscription_detail_page(
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/billing", response_class=HTMLResponse, include_in_schema=False)
|
||||
@router.get("/invoices", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def merchant_billing_history_page(
|
||||
request: Request,
|
||||
current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header),
|
||||
|
||||
@@ -13,8 +13,8 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_store_from_cookie_or_header, get_db
|
||||
from app.modules.core.utils.page_context import get_store_context
|
||||
from app.templates_config import templates
|
||||
from app.modules.tenancy.models import User
|
||||
from app.templates_config import templates
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -12,51 +12,59 @@ Usage:
|
||||
)
|
||||
"""
|
||||
|
||||
from app.modules.billing.schemas.billing import (
|
||||
BillingHistoryListResponse,
|
||||
# Billing History schemas
|
||||
BillingHistoryResponse,
|
||||
BillingHistoryWithMerchant,
|
||||
# Checkout & Portal schemas
|
||||
CheckoutRequest,
|
||||
CheckoutResponse,
|
||||
FeatureCatalogResponse,
|
||||
# Feature Catalog schemas
|
||||
FeatureDeclarationResponse,
|
||||
# Merchant Feature Override schemas
|
||||
MerchantFeatureOverrideEntry,
|
||||
MerchantFeatureOverrideResponse,
|
||||
MerchantSubscriptionAdminCreate,
|
||||
# Merchant Subscription Admin schemas
|
||||
MerchantSubscriptionAdminResponse,
|
||||
MerchantSubscriptionAdminUpdate,
|
||||
MerchantSubscriptionListResponse,
|
||||
MerchantSubscriptionWithMerchant,
|
||||
PortalSessionResponse,
|
||||
# Stats schemas
|
||||
SubscriptionStatsResponse,
|
||||
SubscriptionTierBase,
|
||||
SubscriptionTierCreate,
|
||||
SubscriptionTierListResponse,
|
||||
SubscriptionTierResponse,
|
||||
SubscriptionTierUpdate,
|
||||
# Subscription Tier Admin schemas
|
||||
TierFeatureLimitEntry,
|
||||
)
|
||||
from app.modules.billing.schemas.subscription import (
|
||||
# Tier schemas
|
||||
TierFeatureLimitResponse,
|
||||
TierInfo,
|
||||
# Subscription schemas
|
||||
MerchantSubscriptionCreate,
|
||||
MerchantSubscriptionUpdate,
|
||||
MerchantSubscriptionResponse,
|
||||
MerchantSubscriptionStatusResponse,
|
||||
ChangeTierRequest,
|
||||
ChangeTierResponse,
|
||||
FeatureCheckResponse,
|
||||
# Feature summary schemas
|
||||
FeatureSummaryResponse,
|
||||
# Limit check schemas
|
||||
LimitCheckResult,
|
||||
FeatureCheckResponse,
|
||||
)
|
||||
from app.modules.billing.schemas.billing import (
|
||||
# Subscription Tier Admin schemas
|
||||
TierFeatureLimitEntry,
|
||||
SubscriptionTierBase,
|
||||
SubscriptionTierCreate,
|
||||
SubscriptionTierUpdate,
|
||||
SubscriptionTierResponse,
|
||||
SubscriptionTierListResponse,
|
||||
# Merchant Subscription Admin schemas
|
||||
MerchantSubscriptionAdminResponse,
|
||||
MerchantSubscriptionWithMerchant,
|
||||
MerchantSubscriptionListResponse,
|
||||
MerchantSubscriptionAdminCreate,
|
||||
MerchantSubscriptionAdminUpdate,
|
||||
# Merchant Feature Override schemas
|
||||
MerchantFeatureOverrideEntry,
|
||||
MerchantFeatureOverrideResponse,
|
||||
# Billing History schemas
|
||||
BillingHistoryResponse,
|
||||
BillingHistoryWithMerchant,
|
||||
BillingHistoryListResponse,
|
||||
# Checkout & Portal schemas
|
||||
CheckoutRequest,
|
||||
CheckoutResponse,
|
||||
PortalSessionResponse,
|
||||
# Stats schemas
|
||||
SubscriptionStatsResponse,
|
||||
# Feature Catalog schemas
|
||||
FeatureDeclarationResponse,
|
||||
FeatureCatalogResponse,
|
||||
MerchantPortalAvailableTiersResponse,
|
||||
MerchantPortalInvoiceListResponse,
|
||||
MerchantPortalSubscriptionDetailResponse,
|
||||
# Merchant portal schemas
|
||||
MerchantPortalSubscriptionItem,
|
||||
MerchantPortalSubscriptionListResponse,
|
||||
# Subscription schemas
|
||||
MerchantSubscriptionCreate,
|
||||
MerchantSubscriptionResponse,
|
||||
MerchantSubscriptionStatusResponse,
|
||||
MerchantSubscriptionUpdate,
|
||||
# Tier schemas
|
||||
TierFeatureLimitResponse,
|
||||
TierInfo,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
@@ -73,6 +81,14 @@ __all__ = [
|
||||
# Limit check schemas (subscription.py)
|
||||
"LimitCheckResult",
|
||||
"FeatureCheckResponse",
|
||||
# Merchant portal schemas (subscription.py)
|
||||
"MerchantPortalSubscriptionItem",
|
||||
"MerchantPortalSubscriptionListResponse",
|
||||
"MerchantPortalSubscriptionDetailResponse",
|
||||
"MerchantPortalAvailableTiersResponse",
|
||||
"ChangeTierRequest",
|
||||
"ChangeTierResponse",
|
||||
"MerchantPortalInvoiceListResponse",
|
||||
# Subscription Tier Admin schemas (billing.py)
|
||||
"TierFeatureLimitEntry",
|
||||
"SubscriptionTierBase",
|
||||
|
||||
@@ -9,7 +9,6 @@ from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Subscription Tier Schemas
|
||||
# ============================================================================
|
||||
|
||||
@@ -9,7 +9,6 @@ from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tier Information Schemas
|
||||
# ============================================================================
|
||||
@@ -141,3 +140,82 @@ class FeatureCheckResponse(BaseModel):
|
||||
message: str | None = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Merchant Portal Schemas (for merchant-facing routes)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class MerchantPortalSubscriptionItem(BaseModel):
|
||||
"""Subscription item with tier and platform names for merchant portal list."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
# Base subscription fields (mirror MerchantSubscriptionResponse)
|
||||
id: int
|
||||
merchant_id: int
|
||||
platform_id: int
|
||||
tier_id: int | None
|
||||
status: str
|
||||
is_annual: bool
|
||||
period_start: datetime
|
||||
period_end: datetime
|
||||
trial_ends_at: datetime | None
|
||||
stripe_customer_id: str | None = None
|
||||
cancelled_at: datetime | None = None
|
||||
is_active: bool
|
||||
is_trial: bool
|
||||
trial_days_remaining: int | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
# Enrichment fields
|
||||
tier: str | None = None
|
||||
tier_name: str | None = None
|
||||
platform_name: str = ""
|
||||
|
||||
|
||||
class MerchantPortalSubscriptionListResponse(BaseModel):
|
||||
"""Paginated subscription list for merchant portal."""
|
||||
|
||||
subscriptions: list[MerchantPortalSubscriptionItem]
|
||||
total: int
|
||||
|
||||
|
||||
class MerchantPortalSubscriptionDetailResponse(BaseModel):
|
||||
"""Subscription detail with tier info for merchant portal."""
|
||||
|
||||
subscription: MerchantPortalSubscriptionItem
|
||||
tier: TierInfo | None = None
|
||||
|
||||
|
||||
class MerchantPortalAvailableTiersResponse(BaseModel):
|
||||
"""Available tiers for a platform."""
|
||||
|
||||
tiers: list[dict]
|
||||
current_tier: str | None = None
|
||||
|
||||
|
||||
class ChangeTierRequest(BaseModel):
|
||||
"""Request for changing subscription tier."""
|
||||
|
||||
tier_code: str
|
||||
is_annual: bool = False
|
||||
|
||||
|
||||
class ChangeTierResponse(BaseModel):
|
||||
"""Response after tier change."""
|
||||
|
||||
message: str
|
||||
new_tier: str | None = None
|
||||
effective_immediately: bool = False
|
||||
|
||||
|
||||
class MerchantPortalInvoiceListResponse(BaseModel):
|
||||
"""Paginated invoice list for merchant portal."""
|
||||
|
||||
invoices: list[dict]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@ Billing module services.
|
||||
Provides subscription management, Stripe integration, and admin operations.
|
||||
"""
|
||||
|
||||
from app.modules.billing.services.subscription_service import (
|
||||
SubscriptionService,
|
||||
subscription_service,
|
||||
)
|
||||
from app.modules.billing.services.stripe_service import (
|
||||
StripeService,
|
||||
stripe_service,
|
||||
from app.modules.billing.exceptions import (
|
||||
BillingServiceError,
|
||||
NoActiveSubscriptionError,
|
||||
PaymentSystemNotConfiguredError,
|
||||
StripePriceNotConfiguredError,
|
||||
SubscriptionNotCancelledError,
|
||||
TierNotFoundError,
|
||||
)
|
||||
from app.modules.billing.services.admin_subscription_service import (
|
||||
AdminSubscriptionService,
|
||||
@@ -21,34 +21,34 @@ from app.modules.billing.services.billing_service import (
|
||||
BillingService,
|
||||
billing_service,
|
||||
)
|
||||
from app.modules.billing.exceptions import (
|
||||
BillingServiceError,
|
||||
PaymentSystemNotConfiguredError,
|
||||
TierNotFoundError,
|
||||
StripePriceNotConfiguredError,
|
||||
NoActiveSubscriptionError,
|
||||
SubscriptionNotCancelledError,
|
||||
from app.modules.billing.services.capacity_forecast_service import (
|
||||
CapacityForecastService,
|
||||
capacity_forecast_service,
|
||||
)
|
||||
from app.modules.billing.services.feature_service import (
|
||||
FeatureService,
|
||||
feature_service,
|
||||
)
|
||||
from app.modules.billing.services.capacity_forecast_service import (
|
||||
CapacityForecastService,
|
||||
capacity_forecast_service,
|
||||
)
|
||||
from app.modules.billing.services.platform_pricing_service import (
|
||||
PlatformPricingService,
|
||||
platform_pricing_service,
|
||||
)
|
||||
from app.modules.billing.services.stripe_service import (
|
||||
StripeService,
|
||||
stripe_service,
|
||||
)
|
||||
from app.modules.billing.services.subscription_service import (
|
||||
SubscriptionService,
|
||||
subscription_service,
|
||||
)
|
||||
from app.modules.billing.services.usage_service import (
|
||||
UsageService,
|
||||
usage_service,
|
||||
UsageData,
|
||||
UsageMetricData,
|
||||
LimitCheckData,
|
||||
TierInfoData,
|
||||
UpgradeTierData,
|
||||
LimitCheckData,
|
||||
UsageData,
|
||||
UsageMetricData,
|
||||
UsageService,
|
||||
usage_service,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -324,7 +324,7 @@ class AdminSubscriptionService:
|
||||
.all()
|
||||
)
|
||||
|
||||
tier_distribution = {tier_name: count for tier_name, count in tier_counts}
|
||||
tier_distribution = dict(tier_counts)
|
||||
|
||||
# Calculate MRR (Monthly Recurring Revenue)
|
||||
mrr_cents = 0
|
||||
|
||||
@@ -13,7 +13,6 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from app.modules.contracts.features import (
|
||||
FeatureDeclaration,
|
||||
FeatureProviderProtocol,
|
||||
FeatureScope,
|
||||
FeatureType,
|
||||
FeatureUsage,
|
||||
|
||||
@@ -15,15 +15,6 @@ from datetime import datetime
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.billing.services.stripe_service import stripe_service
|
||||
from app.modules.billing.services.subscription_service import subscription_service
|
||||
from app.modules.billing.models import (
|
||||
AddOnProduct,
|
||||
BillingHistory,
|
||||
MerchantSubscription,
|
||||
SubscriptionTier,
|
||||
StoreAddOn,
|
||||
)
|
||||
from app.modules.billing.exceptions import (
|
||||
BillingServiceError,
|
||||
NoActiveSubscriptionError,
|
||||
@@ -32,6 +23,15 @@ from app.modules.billing.exceptions import (
|
||||
SubscriptionNotCancelledError,
|
||||
TierNotFoundError,
|
||||
)
|
||||
from app.modules.billing.models import (
|
||||
AddOnProduct,
|
||||
BillingHistory,
|
||||
MerchantSubscription,
|
||||
StoreAddOn,
|
||||
SubscriptionTier,
|
||||
)
|
||||
from app.modules.billing.services.stripe_service import stripe_service
|
||||
from app.modules.billing.services.subscription_service import subscription_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -49,7 +49,9 @@ class CapacityForecastService:
|
||||
Should be called by a daily background job.
|
||||
"""
|
||||
from app.modules.cms.services.media_service import media_service
|
||||
from app.modules.monitoring.services.platform_health_service import platform_health_service
|
||||
from app.modules.monitoring.services.platform_health_service import (
|
||||
platform_health_service,
|
||||
)
|
||||
|
||||
now = datetime.now(UTC)
|
||||
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
@@ -234,7 +236,9 @@ class CapacityForecastService:
|
||||
|
||||
Returns prioritized list of recommendations.
|
||||
"""
|
||||
from app.modules.monitoring.services.platform_health_service import platform_health_service
|
||||
from app.modules.monitoring.services.platform_health_service import (
|
||||
platform_health_service,
|
||||
)
|
||||
|
||||
recommendations = []
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ class FeatureAggregatorService:
|
||||
if decl.scope == FeatureScope.STORE and store_id is not None:
|
||||
usage = self.get_store_usage(db, store_id)
|
||||
return usage.get(feature_code)
|
||||
elif decl.scope == FeatureScope.MERCHANT and merchant_id is not None and platform_id is not None:
|
||||
if decl.scope == FeatureScope.MERCHANT and merchant_id is not None and platform_id is not None:
|
||||
usage = self.get_merchant_usage(db, merchant_id, platform_id)
|
||||
return usage.get(feature_code)
|
||||
|
||||
|
||||
@@ -33,9 +33,8 @@ from app.modules.billing.models import (
|
||||
MerchantFeatureOverride,
|
||||
MerchantSubscription,
|
||||
SubscriptionTier,
|
||||
TierFeatureLimit,
|
||||
)
|
||||
from app.modules.contracts.features import FeatureScope, FeatureType
|
||||
from app.modules.contracts.features import FeatureType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -397,7 +396,6 @@ class FeatureService:
|
||||
}
|
||||
|
||||
# Get all usage at once
|
||||
store_usage = {}
|
||||
merchant_usage = feature_aggregator.get_merchant_usage(db, merchant_id, platform_id)
|
||||
|
||||
summaries = []
|
||||
|
||||
@@ -11,7 +11,6 @@ Provides:
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import stripe
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -22,10 +21,7 @@ from app.modules.billing.exceptions import (
|
||||
WebhookVerificationException,
|
||||
)
|
||||
from app.modules.billing.models import (
|
||||
BillingHistory,
|
||||
MerchantSubscription,
|
||||
SubscriptionStatus,
|
||||
SubscriptionTier,
|
||||
)
|
||||
from app.modules.tenancy.models import Store
|
||||
|
||||
|
||||
@@ -29,8 +29,7 @@ from datetime import UTC, datetime, timedelta
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
|
||||
from app.modules.billing.exceptions import (
|
||||
SubscriptionNotFoundException,
|
||||
TierLimitExceededException, # Re-exported for backward compatibility
|
||||
SubscriptionNotFoundException, # Re-exported for backward compatibility
|
||||
)
|
||||
from app.modules.billing.models import (
|
||||
MerchantSubscription,
|
||||
|
||||
@@ -92,7 +92,9 @@ class UsageService:
|
||||
self, db: Session, store_id: int
|
||||
) -> MerchantSubscription | None:
|
||||
"""Resolve store_id to MerchantSubscription."""
|
||||
from app.modules.billing.services.subscription_service import subscription_service
|
||||
from app.modules.billing.services.subscription_service import (
|
||||
subscription_service,
|
||||
)
|
||||
return subscription_service.get_subscription_for_store(db, store_id)
|
||||
|
||||
def get_store_usage(self, db: Session, store_id: int) -> UsageData:
|
||||
|
||||
@@ -12,10 +12,10 @@ Note: capture_capacity_snapshot moved to monitoring module.
|
||||
"""
|
||||
|
||||
from app.modules.billing.tasks.subscription import (
|
||||
reset_period_counters,
|
||||
check_trial_expirations,
|
||||
sync_stripe_status,
|
||||
cleanup_stale_subscriptions,
|
||||
reset_period_counters,
|
||||
sync_stripe_status,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -21,7 +21,6 @@ from app.modules.billing.models import (
|
||||
)
|
||||
from app.modules.tenancy.models import Merchant, Platform, User
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Fixtures
|
||||
# ============================================================================
|
||||
|
||||
@@ -15,7 +15,7 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.api.deps import get_current_merchant_from_cookie_or_header
|
||||
from app.api.deps import get_current_merchant_api, get_merchant_for_current_user
|
||||
from app.modules.billing.models import (
|
||||
BillingHistory,
|
||||
MerchantSubscription,
|
||||
@@ -26,7 +26,6 @@ from app.modules.tenancy.models import Merchant, Platform, User
|
||||
from main import app
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Fixtures
|
||||
# ============================================================================
|
||||
@@ -156,7 +155,7 @@ def merch_invoices(db, merch_merchant):
|
||||
|
||||
@pytest.fixture
|
||||
def merch_auth_headers(merch_owner, merch_merchant):
|
||||
"""Override auth dependency to return a UserContext for the merchant owner."""
|
||||
"""Override auth dependencies to return merchant/user for the merchant owner."""
|
||||
user_context = UserContext(
|
||||
id=merch_owner.id,
|
||||
email=merch_owner.email,
|
||||
@@ -165,13 +164,17 @@ def merch_auth_headers(merch_owner, merch_merchant):
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
def _override():
|
||||
def _override_merchant():
|
||||
return merch_merchant
|
||||
|
||||
def _override_user():
|
||||
return user_context
|
||||
|
||||
app.dependency_overrides[get_current_merchant_from_cookie_or_header] = _override
|
||||
app.dependency_overrides[get_merchant_for_current_user] = _override_merchant
|
||||
app.dependency_overrides[get_current_merchant_api] = _override_user
|
||||
yield {"Authorization": "Bearer fake-token"}
|
||||
if get_current_merchant_from_cookie_or_header in app.dependency_overrides:
|
||||
del app.dependency_overrides[get_current_merchant_from_cookie_or_header]
|
||||
app.dependency_overrides.pop(get_merchant_for_current_user, None)
|
||||
app.dependency_overrides.pop(get_current_merchant_api, None)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@@ -19,7 +19,6 @@ from app.modules.billing.models import (
|
||||
)
|
||||
from app.modules.tenancy.models import Platform
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Fixtures
|
||||
# ============================================================================
|
||||
|
||||
@@ -27,7 +27,6 @@ from app.modules.tenancy.models import Merchant, Platform, Store, User
|
||||
from app.modules.tenancy.models.store import StoreUser, StoreUserType
|
||||
from app.modules.tenancy.models.store_platform import StorePlatform
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Fixtures
|
||||
# ============================================================================
|
||||
|
||||
@@ -22,7 +22,6 @@ from app.modules.billing.services.admin_subscription_service import (
|
||||
)
|
||||
from app.modules.tenancy.models import Merchant
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tier Management
|
||||
# ============================================================================
|
||||
|
||||
@@ -6,6 +6,13 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.billing.models import (
|
||||
AddOnProduct,
|
||||
BillingHistory,
|
||||
MerchantSubscription,
|
||||
SubscriptionStatus,
|
||||
SubscriptionTier,
|
||||
)
|
||||
from app.modules.billing.services.billing_service import (
|
||||
BillingService,
|
||||
NoActiveSubscriptionError,
|
||||
@@ -14,15 +21,6 @@ from app.modules.billing.services.billing_service import (
|
||||
SubscriptionNotCancelledError,
|
||||
TierNotFoundError,
|
||||
)
|
||||
from app.modules.billing.models import (
|
||||
AddOnProduct,
|
||||
BillingHistory,
|
||||
MerchantSubscription,
|
||||
SubscriptionStatus,
|
||||
SubscriptionTier,
|
||||
StoreAddOn,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tier Lookup
|
||||
|
||||
@@ -11,16 +11,15 @@ Tests cover:
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.billing.models import CapacitySnapshot
|
||||
from app.modules.billing.services.capacity_forecast_service import (
|
||||
INFRASTRUCTURE_SCALING,
|
||||
CapacityForecastService,
|
||||
capacity_forecast_service,
|
||||
)
|
||||
from app.modules.billing.models import CapacitySnapshot
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
# tests/unit/services/test_stripe_webhook_handler.py
|
||||
"""Unit tests for StripeWebhookHandler."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import MagicMock, patch
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from app.handlers.stripe_webhook import StripeWebhookHandler
|
||||
from app.modules.billing.models import (
|
||||
BillingHistory,
|
||||
MerchantSubscription,
|
||||
StripeWebhookEvent,
|
||||
SubscriptionStatus,
|
||||
@@ -175,8 +174,8 @@ def test_subscription(db, test_store):
|
||||
store_id=test_store.id,
|
||||
tier="essential",
|
||||
status=SubscriptionStatus.TRIAL,
|
||||
period_start=datetime.now(timezone.utc),
|
||||
period_end=datetime.now(timezone.utc),
|
||||
period_start=datetime.now(UTC),
|
||||
period_end=datetime.now(UTC),
|
||||
)
|
||||
db.add(subscription)
|
||||
db.commit()
|
||||
@@ -207,8 +206,8 @@ def test_active_subscription(db, test_store):
|
||||
status=SubscriptionStatus.ACTIVE,
|
||||
stripe_customer_id="cus_test123",
|
||||
stripe_subscription_id="sub_test123",
|
||||
period_start=datetime.now(timezone.utc),
|
||||
period_end=datetime.now(timezone.utc),
|
||||
period_start=datetime.now(UTC),
|
||||
period_end=datetime.now(UTC),
|
||||
)
|
||||
db.add(subscription)
|
||||
db.commit()
|
||||
@@ -248,8 +247,8 @@ def mock_subscription_updated_event():
|
||||
event.data.object.id = "sub_test123"
|
||||
event.data.object.customer = "cus_test123"
|
||||
event.data.object.status = "active"
|
||||
event.data.object.current_period_start = int(datetime.now(timezone.utc).timestamp())
|
||||
event.data.object.current_period_end = int(datetime.now(timezone.utc).timestamp())
|
||||
event.data.object.current_period_start = int(datetime.now(UTC).timestamp())
|
||||
event.data.object.current_period_end = int(datetime.now(UTC).timestamp())
|
||||
event.data.object.cancel_at_period_end = False
|
||||
event.data.object.items.data = []
|
||||
event.data.object.metadata = {}
|
||||
@@ -277,7 +276,7 @@ def mock_invoice_paid_event():
|
||||
event.data.object.customer = "cus_test123"
|
||||
event.data.object.payment_intent = "pi_test123"
|
||||
event.data.object.number = "INV-001"
|
||||
event.data.object.created = int(datetime.now(timezone.utc).timestamp())
|
||||
event.data.object.created = int(datetime.now(UTC).timestamp())
|
||||
event.data.object.subtotal = 4900
|
||||
event.data.object.tax = 0
|
||||
event.data.object.total = 4900
|
||||
|
||||
@@ -10,11 +10,9 @@ from app.modules.billing.models import (
|
||||
MerchantSubscription,
|
||||
SubscriptionStatus,
|
||||
SubscriptionTier,
|
||||
TierCode,
|
||||
)
|
||||
from app.modules.billing.services.subscription_service import SubscriptionService
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tier Information
|
||||
# ============================================================================
|
||||
|
||||
@@ -8,7 +8,6 @@ It is session-based and does not require customer authentication.
|
||||
|
||||
from app.modules.base import ModuleDefinition, PermissionDefinition
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Router Lazy Imports
|
||||
# =============================================================================
|
||||
|
||||
@@ -4,9 +4,10 @@ Revision ID: cart_001
|
||||
Revises: inventory_001
|
||||
Create Date: 2026-02-07
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision = "cart_001"
|
||||
down_revision = "inventory_001"
|
||||
branch_labels = None
|
||||
|
||||
@@ -15,7 +15,6 @@ from fastapi import APIRouter, Body, Depends, Path
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.modules.cart.services import cart_service
|
||||
from app.modules.cart.schemas import (
|
||||
AddToCartRequest,
|
||||
CartOperationResponse,
|
||||
@@ -23,8 +22,9 @@ from app.modules.cart.schemas import (
|
||||
ClearCartResponse,
|
||||
UpdateCartItemRequest,
|
||||
)
|
||||
from middleware.store_context import require_store_context
|
||||
from app.modules.cart.services import cart_service
|
||||
from app.modules.tenancy.models import Store
|
||||
from middleware.store_context import require_store_context
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
from app.modules.cart.schemas.cart import (
|
||||
AddToCartRequest,
|
||||
UpdateCartItemRequest,
|
||||
CartItemResponse,
|
||||
CartResponse,
|
||||
CartOperationResponse,
|
||||
CartResponse,
|
||||
ClearCartResponse,
|
||||
UpdateCartItemRequest,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# app/modules/cart/services/__init__.py
|
||||
"""Cart module services."""
|
||||
|
||||
from app.modules.cart.services.cart_service import cart_service, CartService
|
||||
from app.modules.cart.services.cart_service import CartService, cart_service
|
||||
|
||||
__all__ = ["cart_service", "CartService"]
|
||||
|
||||
@@ -21,10 +21,10 @@ from app.modules.cart.exceptions import (
|
||||
InsufficientInventoryForCartException,
|
||||
InvalidCartQuantityException,
|
||||
)
|
||||
from app.modules.catalog.exceptions import ProductNotFoundException
|
||||
from app.utils.money import cents_to_euros
|
||||
from app.modules.cart.models.cart import CartItem
|
||||
from app.modules.catalog.exceptions import ProductNotFoundException
|
||||
from app.modules.catalog.models import Product
|
||||
from app.utils.money import cents_to_euros
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from app.modules.base import (
|
||||
)
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Router Lazy Imports
|
||||
# =============================================================================
|
||||
|
||||
@@ -4,9 +4,10 @@ Revision ID: catalog_001
|
||||
Revises: cms_001
|
||||
Create Date: 2026-02-07
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision = "catalog_001"
|
||||
down_revision = "cms_001"
|
||||
branch_labels = None
|
||||
|
||||
@@ -19,7 +19,7 @@ def __getattr__(name: str):
|
||||
if name == "admin_router":
|
||||
from app.modules.catalog.routes.api.admin import admin_router
|
||||
return admin_router
|
||||
elif name == "store_router":
|
||||
if name == "store_router":
|
||||
from app.modules.catalog.routes.api.store import store_router
|
||||
return store_router
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
@@ -18,9 +18,6 @@ from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_current_admin_api, require_module_access
|
||||
from app.core.database import get_db
|
||||
from app.modules.billing.services.subscription_service import subscription_service
|
||||
from app.modules.catalog.services.store_product_service import store_product_service
|
||||
from app.modules.enums import FrontendType
|
||||
from models.schema.auth import UserContext
|
||||
from app.modules.catalog.schemas import (
|
||||
CatalogStore,
|
||||
CatalogStoresResponse,
|
||||
@@ -33,6 +30,9 @@ from app.modules.catalog.schemas import (
|
||||
StoreProductStats,
|
||||
StoreProductUpdate,
|
||||
)
|
||||
from app.modules.catalog.services.store_product_service import store_product_service
|
||||
from app.modules.enums import FrontendType
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
admin_router = APIRouter(
|
||||
prefix="/store-products",
|
||||
|
||||
@@ -15,11 +15,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_store_api, require_module_access
|
||||
from app.core.database import get_db
|
||||
from app.modules.catalog.services.product_service import product_service
|
||||
from app.modules.billing.services.subscription_service import subscription_service
|
||||
from app.modules.catalog.services.store_product_service import store_product_service
|
||||
from app.modules.enums import FrontendType
|
||||
from models.schema.auth import UserContext
|
||||
from app.modules.catalog.schemas import (
|
||||
ProductCreate,
|
||||
ProductDeleteResponse,
|
||||
@@ -31,6 +27,10 @@ from app.modules.catalog.schemas import (
|
||||
StoreDirectProductCreate,
|
||||
StoreProductCreateResponse,
|
||||
)
|
||||
from app.modules.catalog.services.product_service import product_service
|
||||
from app.modules.catalog.services.store_product_service import store_product_service
|
||||
from app.modules.enums import FrontendType
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
store_router = APIRouter(
|
||||
prefix="/products",
|
||||
|
||||
@@ -15,14 +15,14 @@ from fastapi import APIRouter, Depends, Path, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.modules.catalog.services import catalog_service
|
||||
from app.modules.catalog.schemas import (
|
||||
ProductDetailResponse,
|
||||
ProductListResponse,
|
||||
ProductResponse,
|
||||
)
|
||||
from middleware.store_context import require_store_context
|
||||
from app.modules.catalog.services import catalog_service
|
||||
from app.modules.tenancy.models import Store
|
||||
from middleware.store_context import require_store_context
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -14,9 +14,9 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db, require_menu_access
|
||||
from app.modules.core.utils.page_context import get_admin_context
|
||||
from app.templates_config import templates
|
||||
from app.modules.enums import FrontendType
|
||||
from app.modules.tenancy.models import User
|
||||
from app.templates_config import templates
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_store_from_cookie_or_header, get_db
|
||||
from app.modules.core.utils.page_context import get_store_context
|
||||
from app.templates_config import templates
|
||||
from app.modules.tenancy.models import User
|
||||
from app.templates_config import templates
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -3,34 +3,38 @@
|
||||
|
||||
from app.modules.catalog.schemas.catalog import (
|
||||
ProductDetailResponse as CatalogProductDetailResponse,
|
||||
)
|
||||
from app.modules.catalog.schemas.catalog import (
|
||||
ProductListResponse as CatalogProductListResponse,
|
||||
)
|
||||
from app.modules.catalog.schemas.catalog import (
|
||||
ProductResponse as CatalogProductResponse,
|
||||
)
|
||||
from app.modules.catalog.schemas.product import (
|
||||
ProductCreate,
|
||||
ProductUpdate,
|
||||
ProductResponse,
|
||||
ProductDeleteResponse,
|
||||
ProductDetailResponse,
|
||||
ProductListResponse,
|
||||
ProductDeleteResponse,
|
||||
ProductResponse,
|
||||
ProductToggleResponse,
|
||||
ProductUpdate,
|
||||
)
|
||||
from app.modules.catalog.schemas.store_product import (
|
||||
# Catalog store schemas
|
||||
CatalogStore,
|
||||
CatalogStoresResponse,
|
||||
RemoveProductResponse,
|
||||
StoreDirectProductCreate,
|
||||
StoreProductCreate,
|
||||
StoreProductCreateResponse,
|
||||
StoreProductDetail,
|
||||
# List/Detail schemas
|
||||
StoreProductListItem,
|
||||
StoreProductListResponse,
|
||||
StoreProductStats,
|
||||
StoreProductDetail,
|
||||
# Catalog store schemas
|
||||
CatalogStore,
|
||||
CatalogStoresResponse,
|
||||
StoreProductUpdate,
|
||||
# CRUD schemas
|
||||
TranslationUpdate,
|
||||
StoreProductCreate,
|
||||
StoreDirectProductCreate,
|
||||
StoreProductUpdate,
|
||||
StoreProductCreateResponse,
|
||||
RemoveProductResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -8,7 +8,7 @@ For store product management, see the products module.
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from app.modules.inventory.schemas import InventoryLocationResponse
|
||||
from app.modules.marketplace.schemas import MarketplaceProductResponse
|
||||
|
||||
@@ -14,7 +14,6 @@ from sqlalchemy import func
|
||||
|
||||
from app.modules.contracts.features import (
|
||||
FeatureDeclaration,
|
||||
FeatureProviderProtocol,
|
||||
FeatureScope,
|
||||
FeatureType,
|
||||
FeatureUsage,
|
||||
|
||||
@@ -16,9 +16,8 @@ from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.contracts.metrics import (
|
||||
MetricValue,
|
||||
MetricsContext,
|
||||
MetricsProviderProtocol,
|
||||
MetricValue,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -95,7 +94,7 @@ class CatalogMetricsProvider:
|
||||
new_products = new_products_query.count()
|
||||
|
||||
# Products with translations
|
||||
products_with_translations = (
|
||||
(
|
||||
db.query(func.count(func.distinct(Product.id)))
|
||||
.filter(Product.store_id == store_id)
|
||||
.join(Product.translations)
|
||||
|
||||
@@ -18,9 +18,9 @@ from app.modules.catalog.exceptions import (
|
||||
ProductAlreadyExistsException,
|
||||
ProductNotFoundException,
|
||||
)
|
||||
from app.modules.marketplace.models import MarketplaceProduct
|
||||
from app.modules.catalog.models import Product
|
||||
from app.modules.catalog.schemas import ProductCreate, ProductUpdate
|
||||
from app.modules.marketplace.models import MarketplaceProduct
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ Orchestrates payment processing and order creation.
|
||||
|
||||
from app.modules.base import ModuleDefinition, PermissionDefinition
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Router Lazy Imports
|
||||
# =============================================================================
|
||||
|
||||
@@ -18,7 +18,6 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_customer_api
|
||||
from app.core.database import get_db
|
||||
from app.modules.tenancy.exceptions import StoreNotFoundException
|
||||
from app.modules.cart.services import cart_service
|
||||
from app.modules.checkout.schemas import (
|
||||
CheckoutRequest,
|
||||
@@ -27,11 +26,14 @@ from app.modules.checkout.schemas import (
|
||||
)
|
||||
from app.modules.checkout.services import checkout_service
|
||||
from app.modules.customers.schemas import CustomerContext
|
||||
from app.modules.orders.services import order_service
|
||||
from app.modules.messaging.services.email_service import EmailService # noqa: MOD-004 - Core email service
|
||||
from middleware.store_context import require_store_context
|
||||
from app.modules.tenancy.models import Store
|
||||
from app.modules.messaging.services.email_service import (
|
||||
EmailService, # noqa: MOD-004 - Core email service
|
||||
)
|
||||
from app.modules.orders.schemas import OrderCreate, OrderResponse
|
||||
from app.modules.orders.services import order_service
|
||||
from app.modules.tenancy.exceptions import StoreNotFoundException
|
||||
from app.modules.tenancy.models import Store
|
||||
from middleware.store_context import require_store_context
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -15,7 +15,12 @@ This is a self-contained module with:
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition
|
||||
from app.modules.base import (
|
||||
MenuItemDefinition,
|
||||
MenuSectionDefinition,
|
||||
ModuleDefinition,
|
||||
PermissionDefinition,
|
||||
)
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -4,9 +4,10 @@ Revision ID: cms_001
|
||||
Revises: marketplace_001
|
||||
Create Date: 2026-02-07
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision = "cms_001"
|
||||
down_revision = "marketplace_001"
|
||||
branch_labels = None
|
||||
|
||||
@@ -191,10 +191,9 @@ class ContentPage(Base):
|
||||
"""Get the tier level of this page for display purposes."""
|
||||
if self.is_platform_page:
|
||||
return "platform"
|
||||
elif self.store_id is None:
|
||||
if self.store_id is None:
|
||||
return "store_default"
|
||||
else:
|
||||
return "store_override"
|
||||
return "store_override"
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert to dictionary for API responses."""
|
||||
|
||||
@@ -22,10 +22,10 @@ def __getattr__(name: str):
|
||||
if name == "admin_router":
|
||||
from app.modules.cms.routes.admin import admin_router
|
||||
return admin_router
|
||||
elif name == "store_router":
|
||||
if name == "store_router":
|
||||
from app.modules.cms.routes.store import store_router
|
||||
return store_router
|
||||
elif name == "store_media_router":
|
||||
if name == "store_media_router":
|
||||
from app.modules.cms.routes.store import store_media_router
|
||||
return store_media_router
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
@@ -17,8 +17,8 @@ from app.api.deps import get_current_admin_api, get_db
|
||||
from app.exceptions import ValidationException
|
||||
from app.modules.cms.schemas import (
|
||||
ContentPageCreate,
|
||||
ContentPageUpdate,
|
||||
ContentPageResponse,
|
||||
ContentPageUpdate,
|
||||
HomepageSectionsResponse,
|
||||
SectionUpdateResponse,
|
||||
)
|
||||
|
||||
@@ -13,9 +13,9 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.modules.cms.schemas.image import ImageStorageStats
|
||||
from app.modules.cms.services.media_service import media_service
|
||||
from models.schema.auth import UserContext
|
||||
from app.modules.cms.schemas.image import ImageStorageStats
|
||||
|
||||
admin_images_router = APIRouter(prefix="/images")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -12,14 +12,14 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.modules.cms.services.media_service import media_service
|
||||
from models.schema.auth import UserContext
|
||||
from app.modules.cms.schemas.media import (
|
||||
MediaDetailResponse,
|
||||
MediaItemResponse,
|
||||
MediaListResponse,
|
||||
MediaUploadResponse,
|
||||
)
|
||||
from app.modules.cms.services.media_service import media_service
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
admin_media_router = APIRouter(prefix="/media")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -18,15 +18,15 @@ from fastapi import APIRouter, Depends, Path
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api, get_db
|
||||
from app.modules.cms.services.store_theme_service import store_theme_service
|
||||
from models.schema.auth import UserContext
|
||||
from app.modules.cms.schemas.store_theme import (
|
||||
StoreThemeResponse,
|
||||
StoreThemeUpdate,
|
||||
ThemeDeleteResponse,
|
||||
ThemePresetListResponse,
|
||||
ThemePresetResponse,
|
||||
StoreThemeResponse,
|
||||
StoreThemeUpdate,
|
||||
)
|
||||
from app.modules.cms.services.store_theme_service import store_theme_service
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
admin_store_themes_router = APIRouter(prefix="/store-themes")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -19,14 +19,16 @@ from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_current_store_api, get_db
|
||||
from app.modules.cms.exceptions import ContentPageNotFoundException
|
||||
from app.modules.cms.schemas import (
|
||||
CMSUsageResponse,
|
||||
ContentPageResponse,
|
||||
StoreContentPageCreate,
|
||||
StoreContentPageUpdate,
|
||||
ContentPageResponse,
|
||||
CMSUsageResponse,
|
||||
)
|
||||
from app.modules.cms.services import content_page_service
|
||||
from app.modules.tenancy.services.store_service import StoreService # noqa: MOD-004 - shared platform service
|
||||
from app.modules.tenancy.models import User
|
||||
from app.modules.tenancy.services.store_service import (
|
||||
StoreService, # noqa: MOD-004 - shared platform service
|
||||
)
|
||||
|
||||
store_service = StoreService()
|
||||
|
||||
|
||||
@@ -14,9 +14,8 @@ from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_current_store_api
|
||||
from app.core.database import get_db
|
||||
from app.modules.cms.exceptions import MediaOptimizationException
|
||||
from app.modules.cms.services.media_service import media_service
|
||||
from models.schema.auth import UserContext
|
||||
from app.modules.cms.schemas.media import (
|
||||
FailedFileInfo,
|
||||
MediaDetailResponse,
|
||||
MediaItemResponse,
|
||||
MediaListResponse,
|
||||
@@ -26,8 +25,9 @@ from app.modules.cms.schemas.media import (
|
||||
MultipleUploadResponse,
|
||||
OptimizationResultResponse,
|
||||
UploadedFileInfo,
|
||||
FailedFileInfo,
|
||||
)
|
||||
from app.modules.cms.services.media_service import media_service
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
store_media_router = APIRouter(prefix="/media")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -13,8 +13,8 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.modules.cms.schemas import (
|
||||
PublicContentPageResponse,
|
||||
ContentPageListItem,
|
||||
PublicContentPageResponse,
|
||||
)
|
||||
from app.modules.cms.services import content_page_service
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db, require_menu_access
|
||||
from app.templates_config import templates
|
||||
from app.modules.enums import FrontendType
|
||||
from app.modules.tenancy.models import User
|
||||
from app.templates_config import templates
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -13,10 +13,11 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_store_from_cookie_or_header, get_db
|
||||
from app.modules.cms.services import content_page_service
|
||||
from app.modules.core.services.platform_settings_service import platform_settings_service # noqa: MOD-004 - shared platform service
|
||||
from app.modules.core.services.platform_settings_service import (
|
||||
platform_settings_service, # noqa: MOD-004 - shared platform service
|
||||
)
|
||||
from app.modules.tenancy.models import Store, User
|
||||
from app.templates_config import templates
|
||||
from app.modules.tenancy.models import User
|
||||
from app.modules.tenancy.models import Store
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -4,35 +4,42 @@ CMS module Pydantic schemas for API request/response validation.
|
||||
"""
|
||||
|
||||
from app.modules.cms.schemas.content_page import (
|
||||
CMSUsageResponse,
|
||||
# Admin schemas
|
||||
ContentPageCreate,
|
||||
ContentPageUpdate,
|
||||
ContentPageListItem,
|
||||
ContentPageResponse,
|
||||
HomepageSectionsResponse as ContentPageHomepageSectionsResponse,
|
||||
ContentPageUpdate,
|
||||
# Public/Shop schemas
|
||||
PublicContentPageResponse,
|
||||
SectionUpdateResponse,
|
||||
# Store schemas
|
||||
StoreContentPageCreate,
|
||||
StoreContentPageUpdate,
|
||||
CMSUsageResponse,
|
||||
# Public/Shop schemas
|
||||
PublicContentPageResponse,
|
||||
ContentPageListItem,
|
||||
)
|
||||
from app.modules.cms.schemas.content_page import (
|
||||
HomepageSectionsResponse as ContentPageHomepageSectionsResponse,
|
||||
)
|
||||
from app.modules.cms.schemas.homepage_sections import (
|
||||
# Translatable text
|
||||
TranslatableText,
|
||||
CTASection,
|
||||
FeatureCard,
|
||||
FeaturesSection,
|
||||
# Section components
|
||||
HeroButton,
|
||||
HeroSection,
|
||||
FeatureCard,
|
||||
FeaturesSection,
|
||||
PricingSection,
|
||||
CTASection,
|
||||
# Main structure
|
||||
HomepageSections,
|
||||
HomepageSectionsResponse,
|
||||
PricingSection,
|
||||
# API schemas
|
||||
SectionUpdateRequest,
|
||||
HomepageSectionsResponse,
|
||||
# Translatable text
|
||||
TranslatableText,
|
||||
)
|
||||
|
||||
# Image schemas
|
||||
from app.modules.cms.schemas.image import (
|
||||
ImageStorageStats,
|
||||
)
|
||||
|
||||
# Media schemas
|
||||
@@ -51,23 +58,18 @@ from app.modules.cms.schemas.media import (
|
||||
UploadedFileInfo,
|
||||
)
|
||||
|
||||
# Image schemas
|
||||
from app.modules.cms.schemas.image import (
|
||||
ImageStorageStats,
|
||||
)
|
||||
|
||||
# Theme schemas
|
||||
from app.modules.cms.schemas.store_theme import (
|
||||
ThemeDeleteResponse,
|
||||
ThemePresetListResponse,
|
||||
ThemePresetPreview,
|
||||
ThemePresetResponse,
|
||||
StoreThemeBranding,
|
||||
StoreThemeColors,
|
||||
StoreThemeFonts,
|
||||
StoreThemeLayout,
|
||||
StoreThemeResponse,
|
||||
StoreThemeUpdate,
|
||||
ThemeDeleteResponse,
|
||||
ThemePresetListResponse,
|
||||
ThemePresetPreview,
|
||||
ThemePresetResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -10,7 +10,6 @@ Schemas are organized by context:
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ADMIN SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
@@ -18,8 +18,8 @@ Example JSON structure:
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class TranslatableText(BaseModel):
|
||||
@@ -59,13 +59,13 @@ class HeroSection(BaseModel):
|
||||
"""Hero section configuration."""
|
||||
|
||||
enabled: bool = True
|
||||
badge_text: Optional[TranslatableText] = None
|
||||
badge_text: TranslatableText | None = 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
|
||||
background_image: str | None = None
|
||||
buttons: list[HeroButton] = Field(default_factory=list)
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ class FeaturesSection(BaseModel):
|
||||
|
||||
enabled: bool = True
|
||||
title: TranslatableText = Field(default_factory=TranslatableText)
|
||||
subtitle: Optional[TranslatableText] = None
|
||||
subtitle: TranslatableText | None = None
|
||||
features: list[FeatureCard] = Field(default_factory=list)
|
||||
layout: str = Field(default="grid", description="grid, list, cards")
|
||||
|
||||
@@ -92,7 +92,7 @@ class PricingSection(BaseModel):
|
||||
|
||||
enabled: bool = True
|
||||
title: TranslatableText = Field(default_factory=TranslatableText)
|
||||
subtitle: Optional[TranslatableText] = None
|
||||
subtitle: TranslatableText | None = None
|
||||
use_subscription_tiers: bool = Field(
|
||||
default=True, description="Pull pricing from subscription_tiers table dynamically"
|
||||
)
|
||||
@@ -103,7 +103,7 @@ class CTASection(BaseModel):
|
||||
|
||||
enabled: bool = True
|
||||
title: TranslatableText = Field(default_factory=TranslatableText)
|
||||
subtitle: Optional[TranslatableText] = None
|
||||
subtitle: TranslatableText | None = None
|
||||
buttons: list[HeroButton] = Field(default_factory=list)
|
||||
background_type: str = Field(
|
||||
default="gradient", description="gradient, image, solid"
|
||||
@@ -113,10 +113,10 @@ class CTASection(BaseModel):
|
||||
class HomepageSections(BaseModel):
|
||||
"""Complete homepage sections structure."""
|
||||
|
||||
hero: Optional[HeroSection] = None
|
||||
features: Optional[FeaturesSection] = None
|
||||
pricing: Optional[PricingSection] = None
|
||||
cta: Optional[CTASection] = None
|
||||
hero: HeroSection | None = None
|
||||
features: FeaturesSection | None = None
|
||||
pricing: PricingSection | None = None
|
||||
cta: CTASection | None = None
|
||||
|
||||
@classmethod
|
||||
def get_empty_structure(cls, languages: list[str]) -> "HomepageSections":
|
||||
@@ -169,6 +169,6 @@ class SectionUpdateRequest(BaseModel):
|
||||
class HomepageSectionsResponse(BaseModel):
|
||||
"""Response containing all homepage sections with platform language info."""
|
||||
|
||||
sections: Optional[HomepageSections] = None
|
||||
sections: HomepageSections | None = None
|
||||
supported_languages: list[str] = Field(default_factory=lambda: ["fr", "de", "en"])
|
||||
default_language: str = "fr"
|
||||
|
||||
@@ -13,15 +13,15 @@ from app.modules.cms.services.media_service import (
|
||||
MediaService,
|
||||
media_service,
|
||||
)
|
||||
from app.modules.cms.services.store_email_settings_service import (
|
||||
StoreEmailSettingsService,
|
||||
get_store_email_settings_service, # Deprecated: use store_email_settings_service
|
||||
store_email_settings_service,
|
||||
)
|
||||
from app.modules.cms.services.store_theme_service import (
|
||||
StoreThemeService,
|
||||
store_theme_service,
|
||||
)
|
||||
from app.modules.cms.services.store_email_settings_service import (
|
||||
StoreEmailSettingsService,
|
||||
store_email_settings_service,
|
||||
get_store_email_settings_service, # Deprecated: use store_email_settings_service
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ContentPageService",
|
||||
|
||||
@@ -16,7 +16,6 @@ from sqlalchemy import func
|
||||
|
||||
from app.modules.contracts.features import (
|
||||
FeatureDeclaration,
|
||||
FeatureProviderProtocol,
|
||||
FeatureScope,
|
||||
FeatureType,
|
||||
FeatureUsage,
|
||||
|
||||
@@ -15,9 +15,8 @@ from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.contracts.metrics import (
|
||||
MetricValue,
|
||||
MetricsContext,
|
||||
MetricsProviderProtocol,
|
||||
MetricValue,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -96,7 +96,7 @@ class ContentPageService:
|
||||
db.query(ContentPage)
|
||||
.filter(
|
||||
and_(
|
||||
ContentPage.store_id == None,
|
||||
ContentPage.store_id is None,
|
||||
ContentPage.is_platform_page == False,
|
||||
*base_filters,
|
||||
)
|
||||
@@ -136,7 +136,7 @@ class ContentPageService:
|
||||
filters = [
|
||||
ContentPage.platform_id == platform_id,
|
||||
ContentPage.slug == slug,
|
||||
ContentPage.store_id == None,
|
||||
ContentPage.store_id is None,
|
||||
ContentPage.is_platform_page == True,
|
||||
]
|
||||
|
||||
@@ -209,7 +209,7 @@ class ContentPageService:
|
||||
db.query(ContentPage)
|
||||
.filter(
|
||||
and_(
|
||||
ContentPage.store_id == None,
|
||||
ContentPage.store_id is None,
|
||||
ContentPage.is_platform_page == False,
|
||||
*base_filters,
|
||||
)
|
||||
@@ -252,7 +252,7 @@ class ContentPageService:
|
||||
"""
|
||||
filters = [
|
||||
ContentPage.platform_id == platform_id,
|
||||
ContentPage.store_id == None,
|
||||
ContentPage.store_id is None,
|
||||
ContentPage.is_platform_page == True,
|
||||
]
|
||||
|
||||
@@ -291,7 +291,7 @@ class ContentPageService:
|
||||
"""
|
||||
filters = [
|
||||
ContentPage.platform_id == platform_id,
|
||||
ContentPage.store_id == None,
|
||||
ContentPage.store_id is None,
|
||||
ContentPage.is_platform_page == False,
|
||||
]
|
||||
|
||||
@@ -760,12 +760,12 @@ class ContentPageService:
|
||||
|
||||
if page_tier == "platform":
|
||||
filters.append(ContentPage.is_platform_page == True)
|
||||
filters.append(ContentPage.store_id == None)
|
||||
filters.append(ContentPage.store_id is None)
|
||||
elif page_tier == "store_default":
|
||||
filters.append(ContentPage.is_platform_page == False)
|
||||
filters.append(ContentPage.store_id == None)
|
||||
filters.append(ContentPage.store_id is None)
|
||||
elif page_tier == "store_override":
|
||||
filters.append(ContentPage.store_id != None)
|
||||
filters.append(ContentPage.store_id is not None)
|
||||
|
||||
return (
|
||||
db.query(ContentPage)
|
||||
@@ -942,10 +942,10 @@ class ContentPageService:
|
||||
ValueError: If section name is invalid
|
||||
"""
|
||||
from app.modules.cms.schemas import (
|
||||
HeroSection,
|
||||
FeaturesSection,
|
||||
PricingSection,
|
||||
CTASection,
|
||||
FeaturesSection,
|
||||
HeroSection,
|
||||
PricingSection,
|
||||
)
|
||||
|
||||
SECTION_SCHEMAS = {
|
||||
|
||||
@@ -11,7 +11,6 @@ This module provides:
|
||||
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
@@ -22,11 +21,10 @@ from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.cms.exceptions import (
|
||||
MediaFileTooLargeException,
|
||||
MediaNotFoundException,
|
||||
MediaUploadException,
|
||||
MediaValidationException,
|
||||
UnsupportedMediaTypeException,
|
||||
MediaFileTooLargeException,
|
||||
)
|
||||
from app.modules.cms.models import MediaFile
|
||||
|
||||
|
||||
@@ -20,17 +20,16 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import (
|
||||
AuthorizationException,
|
||||
ExternalServiceException,
|
||||
ResourceNotFoundException,
|
||||
ValidationException,
|
||||
ExternalServiceException,
|
||||
)
|
||||
from app.modules.tenancy.models import Store
|
||||
from app.modules.messaging.models import (
|
||||
StoreEmailSettings,
|
||||
EmailProvider,
|
||||
PREMIUM_EMAIL_PROVIDERS,
|
||||
)
|
||||
from app.modules.billing.models import TierCode
|
||||
from app.modules.messaging.models import (
|
||||
PREMIUM_EMAIL_PROVIDERS,
|
||||
EmailProvider,
|
||||
StoreEmailSettings,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -343,7 +342,7 @@ class StoreEmailSettingsService:
|
||||
from_email=(settings.from_email, settings.from_name),
|
||||
to_emails=to_email,
|
||||
subject="Wizamart Email Configuration Test",
|
||||
html_content=f"""
|
||||
html_content="""
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; padding: 20px;">
|
||||
<h2 style="color: #6b46c1;">Email Configuration Test</h2>
|
||||
@@ -376,7 +375,7 @@ class StoreEmailSettingsService:
|
||||
"from": f"{settings.from_name} <{settings.from_email}>",
|
||||
"to": to_email,
|
||||
"subject": "Wizamart Email Configuration Test",
|
||||
"html": f"""
|
||||
"html": """
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; padding: 20px;">
|
||||
<h2 style="color: #6b46c1;">Email Configuration Test</h2>
|
||||
@@ -421,7 +420,7 @@ class StoreEmailSettingsService:
|
||||
"Subject": {"Data": "Wizamart Email Configuration Test"},
|
||||
"Body": {
|
||||
"Html": {
|
||||
"Data": f"""
|
||||
"Data": """
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; padding: 20px;">
|
||||
<h2 style="color: #6b46c1;">Email Configuration Test</h2>
|
||||
|
||||
@@ -11,6 +11,16 @@ import re
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.cms.exceptions import (
|
||||
InvalidColorFormatException,
|
||||
InvalidFontFamilyException,
|
||||
StoreThemeNotFoundException,
|
||||
ThemeOperationException,
|
||||
ThemePresetNotFoundException,
|
||||
ThemeValidationException,
|
||||
)
|
||||
from app.modules.cms.models import StoreTheme
|
||||
from app.modules.cms.schemas.store_theme import StoreThemeUpdate, ThemePresetPreview
|
||||
from app.modules.cms.services.theme_presets import (
|
||||
THEME_PRESETS,
|
||||
apply_preset,
|
||||
@@ -18,17 +28,7 @@ from app.modules.cms.services.theme_presets import (
|
||||
get_preset_preview,
|
||||
)
|
||||
from app.modules.tenancy.exceptions import StoreNotFoundException
|
||||
from app.modules.cms.exceptions import (
|
||||
InvalidColorFormatException,
|
||||
InvalidFontFamilyException,
|
||||
ThemeOperationException,
|
||||
ThemePresetNotFoundException,
|
||||
ThemeValidationException,
|
||||
StoreThemeNotFoundException,
|
||||
)
|
||||
from app.modules.tenancy.models import Store
|
||||
from app.modules.cms.models import StoreTheme
|
||||
from app.modules.cms.schemas.store_theme import ThemePresetPreview, StoreThemeUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -89,13 +89,13 @@ def discover_module_config(module_code: str) -> "BaseSettings | None":
|
||||
|
||||
# First, try to get an instantiated config
|
||||
if hasattr(config_module, "config"):
|
||||
config = getattr(config_module, "config")
|
||||
config = config_module.config
|
||||
logger.debug(f"Loaded config instance for module {module_code}")
|
||||
return config
|
||||
|
||||
# Otherwise, try to instantiate from config_class
|
||||
if hasattr(config_module, "config_class"):
|
||||
config_class = getattr(config_module, "config_class")
|
||||
config_class = config_module.config_class
|
||||
config = config_class()
|
||||
logger.debug(f"Instantiated config class for module {module_code}")
|
||||
return config
|
||||
@@ -121,7 +121,7 @@ def discover_all_module_configs() -> dict[str, "BaseSettings"]:
|
||||
Returns:
|
||||
Dict mapping module code to config instance
|
||||
"""
|
||||
configs: dict[str, "BaseSettings"] = {}
|
||||
configs: dict[str, BaseSettings] = {}
|
||||
|
||||
for module_dir in sorted(MODULES_DIR.iterdir()):
|
||||
if not module_dir.is_dir():
|
||||
|
||||
@@ -59,9 +59,9 @@ from app.modules.contracts.features import (
|
||||
FeatureUsage,
|
||||
)
|
||||
from app.modules.contracts.metrics import (
|
||||
MetricValue,
|
||||
MetricsContext,
|
||||
MetricsProviderProtocol,
|
||||
MetricValue,
|
||||
)
|
||||
from app.modules.contracts.widgets import (
|
||||
BreakdownWidget,
|
||||
|
||||
@@ -47,7 +47,7 @@ Usage:
|
||||
)
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -27,7 +27,6 @@ class ServiceProtocol(Protocol):
|
||||
- Easy testing with mock sessions
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
|
||||
@@ -57,7 +57,7 @@ Usage:
|
||||
"""
|
||||
|
||||
import enum
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user