Templates Migration: - Migrate admin templates to modules (tenancy, billing, monitoring, marketplace, etc.) - Migrate vendor templates to modules (tenancy, billing, orders, messaging, etc.) - Migrate storefront templates to modules (catalog, customers, orders, cart, checkout, cms) - Migrate public templates to modules (billing, marketplace, cms) - Keep shared templates in app/templates/ (base.html, errors/, partials/, macros/) - Migrate letzshop partials to marketplace module Static Files Migration: - Migrate admin JS to modules: tenancy (23 files), core (5 files), monitoring (1 file) - Migrate vendor JS to modules: tenancy (4 files), core (2 files) - Migrate shared JS: vendor-selector.js to core, media-picker.js to cms - Migrate storefront JS: storefront-layout.js to core - Keep framework JS in static/ (api-client, utils, money, icons, log-config, lib/) - Update all template references to use module_static paths Naming Consistency: - Rename static/platform/ to static/public/ - Rename app/templates/platform/ to app/templates/public/ - Update all extends and static references Documentation: - Update module-system.md with shared templates documentation - Update frontend-structure.md with new module JS organization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
314 lines
8.8 KiB
Python
314 lines
8.8 KiB
Python
# app/modules/billing/routes/api/admin_features.py
|
|
"""
|
|
Admin feature management endpoints.
|
|
|
|
Provides endpoints for:
|
|
- Listing all features with their tier assignments
|
|
- Updating tier feature assignments
|
|
- Managing feature metadata
|
|
- Viewing feature usage statistics
|
|
|
|
All routes require module access control for the 'billing' module.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
from pydantic import BaseModel
|
|
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_service import feature_service
|
|
from models.schema.auth import UserContext
|
|
|
|
admin_features_router = APIRouter(
|
|
prefix="/features",
|
|
dependencies=[Depends(require_module_access("billing"))],
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ============================================================================
|
|
# Response Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class FeatureResponse(BaseModel):
|
|
"""Feature information for admin."""
|
|
|
|
id: int
|
|
code: str
|
|
name: str
|
|
description: str | None = None
|
|
category: str
|
|
ui_location: str | None = None
|
|
ui_icon: str | None = None
|
|
ui_route: str | None = None
|
|
ui_badge_text: str | None = None
|
|
minimum_tier_id: int | None = None
|
|
minimum_tier_code: str | None = None
|
|
minimum_tier_name: str | None = None
|
|
is_active: bool
|
|
is_visible: bool
|
|
display_order: int
|
|
|
|
|
|
class FeatureListResponse(BaseModel):
|
|
"""List of features."""
|
|
|
|
features: list[FeatureResponse]
|
|
total: int
|
|
|
|
|
|
class TierFeaturesResponse(BaseModel):
|
|
"""Tier with its features."""
|
|
|
|
id: int
|
|
code: str
|
|
name: str
|
|
description: str | None = None
|
|
features: list[str]
|
|
feature_count: int
|
|
|
|
|
|
class TierListWithFeaturesResponse(BaseModel):
|
|
"""All tiers with their features."""
|
|
|
|
tiers: list[TierFeaturesResponse]
|
|
|
|
|
|
class UpdateTierFeaturesRequest(BaseModel):
|
|
"""Request to update tier features."""
|
|
|
|
feature_codes: list[str]
|
|
|
|
|
|
class UpdateFeatureRequest(BaseModel):
|
|
"""Request to update feature metadata."""
|
|
|
|
name: str | None = None
|
|
description: str | None = None
|
|
category: str | None = None
|
|
ui_location: str | None = None
|
|
ui_icon: str | None = None
|
|
ui_route: str | None = None
|
|
ui_badge_text: str | None = None
|
|
minimum_tier_code: str | None = None
|
|
is_active: bool | None = None
|
|
is_visible: bool | None = None
|
|
display_order: int | None = None
|
|
|
|
|
|
class CategoryListResponse(BaseModel):
|
|
"""List of feature categories."""
|
|
|
|
categories: list[str]
|
|
|
|
|
|
class TierFeatureDetailResponse(BaseModel):
|
|
"""Tier features with full details."""
|
|
|
|
tier_code: str
|
|
tier_name: str
|
|
features: list[dict]
|
|
feature_count: int
|
|
|
|
|
|
# ============================================================================
|
|
# Helper Functions
|
|
# ============================================================================
|
|
|
|
|
|
def _feature_to_response(feature) -> FeatureResponse:
|
|
"""Convert Feature model to response."""
|
|
return FeatureResponse(
|
|
id=feature.id,
|
|
code=feature.code,
|
|
name=feature.name,
|
|
description=feature.description,
|
|
category=feature.category,
|
|
ui_location=feature.ui_location,
|
|
ui_icon=feature.ui_icon,
|
|
ui_route=feature.ui_route,
|
|
ui_badge_text=feature.ui_badge_text,
|
|
minimum_tier_id=feature.minimum_tier_id,
|
|
minimum_tier_code=feature.minimum_tier.code if feature.minimum_tier else None,
|
|
minimum_tier_name=feature.minimum_tier.name if feature.minimum_tier else None,
|
|
is_active=feature.is_active,
|
|
is_visible=feature.is_visible,
|
|
display_order=feature.display_order,
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Endpoints
|
|
# ============================================================================
|
|
|
|
|
|
@admin_features_router.get("", response_model=FeatureListResponse)
|
|
def list_features(
|
|
category: str | None = Query(None, description="Filter by category"),
|
|
active_only: bool = Query(False, description="Only active features"),
|
|
current_user: UserContext = Depends(get_current_admin_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""List all features with their tier assignments."""
|
|
features = feature_service.get_all_features(
|
|
db, category=category, active_only=active_only
|
|
)
|
|
|
|
return FeatureListResponse(
|
|
features=[_feature_to_response(f) for f in features],
|
|
total=len(features),
|
|
)
|
|
|
|
|
|
@admin_features_router.get("/categories", response_model=CategoryListResponse)
|
|
def list_categories(
|
|
current_user: UserContext = Depends(get_current_admin_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""List all feature categories."""
|
|
categories = feature_service.get_categories(db)
|
|
return CategoryListResponse(categories=categories)
|
|
|
|
|
|
@admin_features_router.get("/tiers", response_model=TierListWithFeaturesResponse)
|
|
def list_tiers_with_features(
|
|
current_user: UserContext = Depends(get_current_admin_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""List all tiers with their feature assignments."""
|
|
tiers = feature_service.get_all_tiers_with_features(db)
|
|
|
|
return TierListWithFeaturesResponse(
|
|
tiers=[
|
|
TierFeaturesResponse(
|
|
id=t.id,
|
|
code=t.code,
|
|
name=t.name,
|
|
description=t.description,
|
|
features=t.features or [],
|
|
feature_count=len(t.features or []),
|
|
)
|
|
for t in tiers
|
|
]
|
|
)
|
|
|
|
|
|
@admin_features_router.get("/{feature_code}", response_model=FeatureResponse)
|
|
def get_feature(
|
|
feature_code: str,
|
|
current_user: UserContext = Depends(get_current_admin_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Get a single feature by code.
|
|
|
|
Raises 404 if feature not found.
|
|
"""
|
|
feature = feature_service.get_feature_by_code(db, feature_code)
|
|
|
|
if not feature:
|
|
from app.modules.billing.exceptions import FeatureNotFoundError
|
|
|
|
raise FeatureNotFoundError(feature_code)
|
|
|
|
return _feature_to_response(feature)
|
|
|
|
|
|
@admin_features_router.put("/{feature_code}", response_model=FeatureResponse)
|
|
def update_feature(
|
|
feature_code: str,
|
|
request: UpdateFeatureRequest,
|
|
current_user: UserContext = Depends(get_current_admin_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Update feature metadata.
|
|
|
|
Raises 404 if feature not found, 400 if tier code is invalid.
|
|
"""
|
|
feature = feature_service.update_feature(
|
|
db,
|
|
feature_code,
|
|
name=request.name,
|
|
description=request.description,
|
|
category=request.category,
|
|
ui_location=request.ui_location,
|
|
ui_icon=request.ui_icon,
|
|
ui_route=request.ui_route,
|
|
ui_badge_text=request.ui_badge_text,
|
|
minimum_tier_code=request.minimum_tier_code,
|
|
is_active=request.is_active,
|
|
is_visible=request.is_visible,
|
|
display_order=request.display_order,
|
|
)
|
|
|
|
db.commit()
|
|
db.refresh(feature)
|
|
|
|
logger.info(f"Updated feature {feature_code} by admin {current_user.id}")
|
|
|
|
return _feature_to_response(feature)
|
|
|
|
|
|
@admin_features_router.put("/tiers/{tier_code}/features", response_model=TierFeaturesResponse)
|
|
def update_tier_features(
|
|
tier_code: str,
|
|
request: UpdateTierFeaturesRequest,
|
|
current_user: UserContext = Depends(get_current_admin_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Update features assigned to a tier.
|
|
|
|
Raises 404 if tier not found, 422 if any feature codes are invalid.
|
|
"""
|
|
tier = feature_service.update_tier_features(db, tier_code, request.feature_codes)
|
|
db.commit()
|
|
|
|
logger.info(
|
|
f"Updated tier {tier_code} features to {len(request.feature_codes)} features "
|
|
f"by admin {current_user.id}"
|
|
)
|
|
|
|
return TierFeaturesResponse(
|
|
id=tier.id,
|
|
code=tier.code,
|
|
name=tier.name,
|
|
description=tier.description,
|
|
features=tier.features or [],
|
|
feature_count=len(tier.features or []),
|
|
)
|
|
|
|
|
|
@admin_features_router.get("/tiers/{tier_code}/features", response_model=TierFeatureDetailResponse)
|
|
def get_tier_features(
|
|
tier_code: str,
|
|
current_user: UserContext = Depends(get_current_admin_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Get features assigned to a specific tier with full details.
|
|
|
|
Raises 404 if tier not found.
|
|
"""
|
|
tier, features = feature_service.get_tier_features_with_details(db, tier_code)
|
|
|
|
return TierFeatureDetailResponse(
|
|
tier_code=tier.code,
|
|
tier_name=tier.name,
|
|
features=[
|
|
{
|
|
"code": f.code,
|
|
"name": f.name,
|
|
"category": f.category,
|
|
"description": f.description,
|
|
}
|
|
for f in features
|
|
],
|
|
feature_count=len(features),
|
|
)
|