MIGRATION: - Delete app/api/v1/vendor/analytics.py (duplicate - analytics module already auto-discovered) - Move usage routes from app/api/v1/vendor/usage.py to billing module - Move onboarding routes from app/api/v1/vendor/onboarding.py to marketplace module - Move features routes to billing module (admin + vendor) - Move inventory routes to inventory module (admin + vendor) - Move marketplace/letzshop routes to marketplace module - Move orders routes to orders module - Delete legacy letzshop service files (moved to marketplace module) DOCUMENTATION: - Add docs/development/migration/module-autodiscovery-migration.md with full migration history - Update docs/architecture/module-system.md with Entity Auto-Discovery Reference section - Add detailed sections for each entity type: routes, services, models, schemas, tasks, exceptions, templates, static files, locales, configuration ARCHITECTURE VALIDATION: - Add MOD-016: Routes must be in modules, not app/api/v1/ - Add MOD-017: Services must be in modules, not app/services/ - Add MOD-018: Tasks must be in modules, not app/tasks/ - Add MOD-019: Schemas must be in modules, not models/schema/ - Update scripts/validate_architecture.py with _validate_legacy_locations method - Update .architecture-rules/module.yaml with legacy location rules These rules enforce that all entities must be in self-contained modules. Legacy locations now trigger ERROR severity violations. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
354 lines
10 KiB
Python
354 lines
10 KiB
Python
# 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)
|