# app/modules/billing/routes/api/vendor_features.py """ Vendor features API endpoints. Provides feature availability information for the frontend to: - Show/hide UI elements based on tier - Display upgrade prompts for unavailable features - Load feature metadata for dynamic rendering Endpoints: - GET /features/available - List of feature codes (for quick checks) - GET /features - Full feature list with availability and metadata - GET /features/{code} - Single feature details with upgrade info - GET /features/categories - List feature categories 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_vendor_api, require_module_access from app.core.database import get_db from app.exceptions import FeatureNotFoundError from app.services.feature_service import feature_service from models.schema.auth import UserContext vendor_features_router = APIRouter( prefix="/features", dependencies=[Depends(require_module_access("billing"))], ) logger = logging.getLogger(__name__) # ============================================================================ # Response Schemas # ============================================================================ class FeatureCodeListResponse(BaseModel): """Simple list of available feature codes for quick checks.""" features: list[str] tier_code: str tier_name: str class FeatureResponse(BaseModel): """Full feature information.""" 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 is_available: bool minimum_tier_code: str | None = None minimum_tier_name: str | None = None class FeatureListResponse(BaseModel): """List of features with metadata.""" features: list[FeatureResponse] available_count: int total_count: int tier_code: str tier_name: str class FeatureDetailResponse(BaseModel): """Single feature detail with upgrade info.""" 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 is_available: bool # Upgrade info (only if not available) upgrade_tier_code: str | None = None upgrade_tier_name: str | None = None upgrade_tier_price_monthly_cents: int | None = None class CategoryListResponse(BaseModel): """List of feature categories.""" categories: list[str] class FeatureGroupedResponse(BaseModel): """Features grouped by category.""" categories: dict[str, list[FeatureResponse]] available_count: int total_count: int class FeatureCheckResponse(BaseModel): """Quick feature availability check response.""" has_feature: bool feature_code: str # ============================================================================ # Endpoints # ============================================================================ @vendor_features_router.get("/available", response_model=FeatureCodeListResponse) def get_available_features( current_user: UserContext = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Get list of feature codes available to vendor. This is a lightweight endpoint for quick feature checks. Use this to populate a frontend feature store on app init. Returns: List of feature codes the vendor has access to """ vendor_id = current_user.token_vendor_id # Get subscription for tier info from app.services.subscription_service import subscription_service subscription = subscription_service.get_or_create_subscription(db, vendor_id) tier = subscription.tier_obj # Get available features feature_codes = feature_service.get_available_feature_codes(db, vendor_id) return FeatureCodeListResponse( features=feature_codes, tier_code=subscription.tier, tier_name=tier.name if tier else subscription.tier.title(), ) @vendor_features_router.get("", response_model=FeatureListResponse) def get_features( category: str | None = Query(None, description="Filter by category"), include_unavailable: bool = Query(True, description="Include features not available to vendor"), current_user: UserContext = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Get all features with availability status and metadata. This is a comprehensive endpoint for building feature-gated UIs. Each feature includes: - Availability status - UI metadata (icon, route, location) - Minimum tier required Args: category: Filter to specific category (orders, inventory, etc.) include_unavailable: Whether to include locked features Returns: List of features with metadata and availability """ vendor_id = current_user.token_vendor_id # Get subscription for tier info from app.services.subscription_service import subscription_service subscription = subscription_service.get_or_create_subscription(db, vendor_id) tier = subscription.tier_obj # Get features features = feature_service.get_vendor_features( db, vendor_id, category=category, include_unavailable=include_unavailable, ) available_count = sum(1 for f in features if f.is_available) return FeatureListResponse( features=[ FeatureResponse( 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, is_available=f.is_available, minimum_tier_code=f.minimum_tier_code, minimum_tier_name=f.minimum_tier_name, ) for f in features ], available_count=available_count, total_count=len(features), tier_code=subscription.tier, tier_name=tier.name if tier else subscription.tier.title(), ) @vendor_features_router.get("/categories", response_model=CategoryListResponse) def get_feature_categories( current_user: UserContext = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Get list of feature categories. Returns: List of category names """ categories = feature_service.get_categories(db) return CategoryListResponse(categories=categories) @vendor_features_router.get("/grouped", response_model=FeatureGroupedResponse) def get_features_grouped( current_user: UserContext = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Get features grouped by category. Useful for rendering feature comparison tables or settings pages. """ vendor_id = current_user.token_vendor_id grouped = feature_service.get_features_grouped_by_category(db, vendor_id) # Convert to response format categories_response = {} total = 0 available = 0 for category, features in grouped.items(): categories_response[category] = [ FeatureResponse( 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, is_available=f.is_available, minimum_tier_code=f.minimum_tier_code, minimum_tier_name=f.minimum_tier_name, ) for f in features ] total += len(features) available += sum(1 for f in features if f.is_available) return FeatureGroupedResponse( categories=categories_response, available_count=available, total_count=total, ) @vendor_features_router.get("/{feature_code}", response_model=FeatureDetailResponse) def get_feature_detail( feature_code: str, current_user: UserContext = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Get detailed information about a specific feature. Includes upgrade information if the feature is not available. Use this for upgrade prompts and feature explanation modals. Args: feature_code: The feature code Returns: Feature details with upgrade info if locked """ vendor_id = current_user.token_vendor_id # Get feature feature = feature_service.get_feature_by_code(db, feature_code) if not feature: raise FeatureNotFoundError(feature_code) # Check availability is_available = feature_service.has_feature(db, vendor_id, feature_code) # Get upgrade info if not available upgrade_tier_code = None upgrade_tier_name = None upgrade_tier_price = None if not is_available: upgrade_info = feature_service.get_feature_upgrade_info(db, feature_code) if upgrade_info: upgrade_tier_code = upgrade_info.required_tier_code upgrade_tier_name = upgrade_info.required_tier_name upgrade_tier_price = upgrade_info.required_tier_price_monthly_cents return FeatureDetailResponse( 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, is_available=is_available, upgrade_tier_code=upgrade_tier_code, upgrade_tier_name=upgrade_tier_name, upgrade_tier_price_monthly_cents=upgrade_tier_price, ) @vendor_features_router.get("/check/{feature_code}", response_model=FeatureCheckResponse) def check_feature( feature_code: str, current_user: UserContext = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Quick check if vendor has access to a feature. Returns simple boolean response for inline checks. Args: feature_code: The feature code Returns: has_feature and feature_code """ vendor_id = current_user.token_vendor_id has_feature = feature_service.has_feature(db, vendor_id, feature_code) return FeatureCheckResponse(has_feature=has_feature, feature_code=feature_code)