# 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, HTTPException, 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.database.feature import Feature from models.database.subscription import SubscriptionTier from models.database.user import User 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] # ============================================================================ # 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: User = 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=[ FeatureResponse( id=f.id, code=f.code, name=f.name, description=f.description, category=f.category, ui_location=f.ui_location, ui_icon=f.ui_icon, ui_route=f.ui_route, ui_badge_text=f.ui_badge_text, minimum_tier_id=f.minimum_tier_id, minimum_tier_code=f.minimum_tier.code if f.minimum_tier else None, minimum_tier_name=f.minimum_tier.name if f.minimum_tier else None, is_active=f.is_active, is_visible=f.is_visible, display_order=f.display_order, ) for f in features ], total=len(features), ) @router.get("/categories", response_model=CategoryListResponse) def list_categories( current_user: User = 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: User = Depends(get_current_admin_api), db: Session = Depends(get_db), ): """List all tiers with their feature assignments.""" tiers = ( db.query(SubscriptionTier) .filter(SubscriptionTier.is_active == True) # noqa: E712 .order_by(SubscriptionTier.display_order) .all() ) 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: User = Depends(get_current_admin_api), db: Session = Depends(get_db), ): """Get a single feature by code.""" feature = feature_service.get_feature_by_code(db, feature_code) if not feature: raise HTTPException(status_code=404, detail=f"Feature '{feature_code}' not found") 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, ) @router.put("/{feature_code}", response_model=FeatureResponse) def update_feature( feature_code: str, request: UpdateFeatureRequest, current_user: User = Depends(get_current_admin_api), db: Session = Depends(get_db), ): """Update feature metadata.""" feature = db.query(Feature).filter(Feature.code == feature_code).first() if not feature: raise HTTPException(status_code=404, detail=f"Feature '{feature_code}' not found") # Update fields if provided if request.name is not None: feature.name = request.name if request.description is not None: feature.description = request.description if request.category is not None: feature.category = request.category if request.ui_location is not None: feature.ui_location = request.ui_location if request.ui_icon is not None: feature.ui_icon = request.ui_icon if request.ui_route is not None: feature.ui_route = request.ui_route if request.ui_badge_text is not None: feature.ui_badge_text = request.ui_badge_text if request.is_active is not None: feature.is_active = request.is_active if request.is_visible is not None: feature.is_visible = request.is_visible if request.display_order is not None: feature.display_order = request.display_order # Update minimum tier if provided if request.minimum_tier_code is not None: if request.minimum_tier_code == "": feature.minimum_tier_id = None else: tier = ( db.query(SubscriptionTier) .filter(SubscriptionTier.code == request.minimum_tier_code) .first() ) if not tier: raise HTTPException( status_code=400, detail=f"Tier '{request.minimum_tier_code}' not found", ) feature.minimum_tier_id = tier.id db.commit() db.refresh(feature) logger.info(f"Updated feature {feature_code} by admin {current_user.id}") 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, ) @router.put("/tiers/{tier_code}/features", response_model=TierFeaturesResponse) def update_tier_features( tier_code: str, request: UpdateTierFeaturesRequest, current_user: User = Depends(get_current_admin_api), db: Session = Depends(get_db), ): """Update features assigned to a tier.""" try: 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 []), ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @router.get("/tiers/{tier_code}/features") def get_tier_features( tier_code: str, current_user: User = Depends(get_current_admin_api), db: Session = Depends(get_db), ): """Get features assigned to a specific tier.""" tier = db.query(SubscriptionTier).filter(SubscriptionTier.code == tier_code).first() if not tier: raise HTTPException(status_code=404, detail=f"Tier '{tier_code}' not found") # Get full feature details for the tier's features feature_codes = tier.features or [] features = ( db.query(Feature) .filter(Feature.code.in_(feature_codes)) .order_by(Feature.category, Feature.display_order) .all() ) return { "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), }