# 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 app.modules.enums import FrontendType from models.schema.auth import UserContext admin_features_router = APIRouter( prefix="/features", dependencies=[Depends(require_module_access("billing", FrontendType.ADMIN))], ) 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), )