Some checks failed
Move all auth schemas (UserContext, UserLogin, LoginResponse, etc.) from legacy models/schema/auth.py to app/modules/tenancy/schemas/auth.py per MOD-019. Update 84 import sites across 14 modules. Legacy file now re-exports for backwards compatibility. Add missing tenancy service methods for cross-module consumers: - merchant_service.get_merchant_by_owner_id() - merchant_service.get_merchant_count_for_owner() - admin_service.get_user_by_id() (public, was private-only) - platform_service.get_active_store_count() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
296 lines
9.7 KiB
Python
296 lines
9.7 KiB
Python
# app/modules/billing/routes/api/store_features.py
|
|
"""
|
|
Store 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
|
|
- GET /features/check/{code} - Quick boolean feature check
|
|
|
|
All routes require module access control for the 'billing' module.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.api.deps import get_current_store_api, require_module_access
|
|
from app.core.database import get_db
|
|
from app.modules.billing.exceptions import FeatureNotFoundException
|
|
from app.modules.billing.schemas.billing import (
|
|
CategoryListResponse,
|
|
FeatureCodeListResponse,
|
|
FeatureDetailResponse,
|
|
FeatureGroupedResponse,
|
|
FeatureListResponse,
|
|
FeatureResponse,
|
|
StoreFeatureCheckResponse,
|
|
)
|
|
from app.modules.billing.services.feature_aggregator import feature_aggregator
|
|
from app.modules.billing.services.feature_service import feature_service
|
|
from app.modules.billing.services.subscription_service import subscription_service
|
|
from app.modules.enums import FrontendType
|
|
from app.modules.tenancy.schemas.auth import UserContext
|
|
|
|
store_features_router = APIRouter(
|
|
prefix="/features",
|
|
dependencies=[Depends(require_module_access("billing", FrontendType.STORE))],
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ============================================================================
|
|
# Internal Helpers
|
|
# ============================================================================
|
|
|
|
|
|
def _get_tier_info(db: Session, store_id: int) -> tuple[str, str]:
|
|
"""Get (tier_code, tier_name) for a store's subscription."""
|
|
sub = subscription_service.get_subscription_for_store(db, store_id)
|
|
if sub and sub.tier:
|
|
return sub.tier.code, sub.tier.name
|
|
return "unknown", "Unknown"
|
|
|
|
|
|
def _declaration_to_feature_response(
|
|
decl, is_available: bool
|
|
) -> FeatureResponse:
|
|
"""Map a FeatureDeclaration to a FeatureResponse."""
|
|
return FeatureResponse(
|
|
code=decl.code,
|
|
name=decl.name_key,
|
|
description=decl.description_key,
|
|
category=decl.category,
|
|
feature_type=decl.feature_type.value if decl.feature_type else None,
|
|
ui_icon=decl.ui_icon,
|
|
is_available=is_available,
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Endpoints
|
|
# ============================================================================
|
|
|
|
|
|
@store_features_router.get("/available", response_model=FeatureCodeListResponse)
|
|
def get_available_features(
|
|
current_user: UserContext = Depends(get_current_store_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Get list of feature codes available to store.
|
|
|
|
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 store has access to
|
|
"""
|
|
store_id = current_user.token_store_id
|
|
merchant_id, platform_id = subscription_service.resolve_store_to_merchant(db, store_id)
|
|
|
|
# Get available feature codes
|
|
feature_codes = feature_service.get_merchant_feature_codes(db, merchant_id, platform_id)
|
|
|
|
# Get tier info
|
|
tier_code, tier_name = _get_tier_info(db, store_id)
|
|
|
|
return FeatureCodeListResponse(
|
|
features=sorted(feature_codes),
|
|
tier_code=tier_code,
|
|
tier_name=tier_name,
|
|
)
|
|
|
|
|
|
@store_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 store"),
|
|
current_user: UserContext = Depends(get_current_store_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)
|
|
- Feature type (binary/quantitative)
|
|
|
|
Args:
|
|
category: Filter to specific category (orders, inventory, etc.)
|
|
include_unavailable: Whether to include locked features
|
|
|
|
Returns:
|
|
List of features with metadata and availability
|
|
"""
|
|
store_id = current_user.token_store_id
|
|
merchant_id, platform_id = subscription_service.resolve_store_to_merchant(db, store_id)
|
|
|
|
# Get all declarations and available codes
|
|
all_declarations = feature_aggregator.get_all_declarations()
|
|
available_codes = feature_service.get_merchant_feature_codes(db, merchant_id, platform_id)
|
|
|
|
# Build feature list
|
|
features = []
|
|
for code, decl in sorted(
|
|
all_declarations.items(), key=lambda x: (x[1].category, x[1].display_order)
|
|
):
|
|
# Filter by category if specified
|
|
if category and decl.category != category:
|
|
continue
|
|
|
|
is_available = code in available_codes
|
|
|
|
# Skip unavailable if not requested
|
|
if not include_unavailable and not is_available:
|
|
continue
|
|
|
|
features.append(_declaration_to_feature_response(decl, is_available))
|
|
|
|
available_count = sum(1 for f in features if f.is_available)
|
|
|
|
# Get tier info
|
|
tier_code, tier_name = _get_tier_info(db, store_id)
|
|
|
|
return FeatureListResponse(
|
|
features=features,
|
|
available_count=available_count,
|
|
total_count=len(features),
|
|
tier_code=tier_code,
|
|
tier_name=tier_name,
|
|
)
|
|
|
|
|
|
@store_features_router.get("/categories", response_model=CategoryListResponse)
|
|
def get_feature_categories(
|
|
current_user: UserContext = Depends(get_current_store_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Get list of feature categories.
|
|
|
|
Returns:
|
|
List of category names
|
|
"""
|
|
by_category = feature_aggregator.get_declarations_by_category()
|
|
return CategoryListResponse(categories=sorted(by_category.keys()))
|
|
|
|
|
|
@store_features_router.get("/grouped", response_model=FeatureGroupedResponse)
|
|
def get_features_grouped(
|
|
current_user: UserContext = Depends(get_current_store_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Get features grouped by category.
|
|
|
|
Useful for rendering feature comparison tables or settings pages.
|
|
"""
|
|
store_id = current_user.token_store_id
|
|
merchant_id, platform_id = subscription_service.resolve_store_to_merchant(db, store_id)
|
|
|
|
# Get declarations grouped by category and available codes
|
|
by_category = feature_aggregator.get_declarations_by_category()
|
|
available_codes = feature_service.get_merchant_feature_codes(db, merchant_id, platform_id)
|
|
|
|
# Convert to response format
|
|
categories_response: dict[str, list[FeatureResponse]] = {}
|
|
total = 0
|
|
available = 0
|
|
|
|
for category, declarations in sorted(by_category.items()):
|
|
category_features = []
|
|
for decl in declarations:
|
|
is_available = decl.code in available_codes
|
|
category_features.append(
|
|
_declaration_to_feature_response(decl, is_available)
|
|
)
|
|
total += 1
|
|
if is_available:
|
|
available += 1
|
|
categories_response[category] = category_features
|
|
|
|
return FeatureGroupedResponse(
|
|
categories=categories_response,
|
|
available_count=available,
|
|
total_count=total,
|
|
)
|
|
|
|
|
|
@store_features_router.get("/check/{feature_code}", response_model=StoreFeatureCheckResponse)
|
|
def check_feature(
|
|
feature_code: str,
|
|
current_user: UserContext = Depends(get_current_store_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Quick check if store has access to a feature.
|
|
|
|
Returns simple boolean response for inline checks.
|
|
Uses has_feature_for_store which resolves store -> merchant internally.
|
|
|
|
Args:
|
|
feature_code: The feature code
|
|
|
|
Returns:
|
|
has_feature and feature_code
|
|
"""
|
|
store_id = current_user.token_store_id
|
|
has = feature_service.has_feature_for_store(db, store_id, feature_code)
|
|
|
|
return StoreFeatureCheckResponse(has_feature=has, feature_code=feature_code)
|
|
|
|
|
|
@store_features_router.get("/{feature_code}", response_model=FeatureDetailResponse)
|
|
def get_feature_detail(
|
|
feature_code: str,
|
|
current_user: UserContext = Depends(get_current_store_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
|
|
"""
|
|
store_id = current_user.token_store_id
|
|
merchant_id, platform_id = subscription_service.resolve_store_to_merchant(db, store_id)
|
|
|
|
# Get feature declaration
|
|
decl = feature_aggregator.get_declaration(feature_code)
|
|
if not decl:
|
|
raise FeatureNotFoundException(feature_code)
|
|
|
|
# Check availability
|
|
is_available = feature_service.has_feature(db, merchant_id, platform_id, feature_code)
|
|
|
|
# Build response
|
|
return FeatureDetailResponse(
|
|
code=decl.code,
|
|
name=decl.name_key,
|
|
description=decl.description_key,
|
|
category=decl.category,
|
|
feature_type=decl.feature_type.value if decl.feature_type else None,
|
|
ui_icon=decl.ui_icon,
|
|
is_available=is_available,
|
|
# Upgrade info fields are left as None since the new service
|
|
# does not provide tier-comparison upgrade suggestions.
|
|
# This can be extended when upgrade flow is implemented.
|
|
)
|