# app/api/v1/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 """ 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 from app.core.database import get_db from app.services.feature_service import feature_service from models.schema.auth import UserContext router = APIRouter(prefix="/features") 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 # ============================================================================ @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), ) @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) @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 ] ) @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.exceptions import FeatureNotFoundError raise FeatureNotFoundError(feature_code) return _feature_to_response(feature) @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) @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 []), ) @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), )