""" License API endpoints for WHOOSH platform. Provides secure proxy to KACHING license authority and implements license-aware user experiences. This module implements Phase 3A of the WHOOSH licensing integration plan: - Backend proxy pattern to avoid exposing license IDs in frontend - Secure server-side license status resolution - User organization to license mapping - License status, quota, and upgrade suggestion endpoints Business Logic: - All license operations are resolved server-side for security - Users see their license tier, quotas, and usage without accessing raw license IDs - Upgrade suggestions are generated based on usage patterns and tier limitations - Feature availability is determined server-side to prevent client-side bypass """ from datetime import datetime, timedelta from typing import List, Optional, Dict, Any from fastapi import APIRouter, Depends, HTTPException, status, Request from sqlalchemy.orm import Session from pydantic import BaseModel import httpx import asyncio import os import logging from app.core.database import get_db from app.core.auth_deps import get_current_active_user from app.models.user import User logger = logging.getLogger(__name__) router = APIRouter() # Environment configuration for KACHING integration KACHING_BASE_URL = os.getenv("KACHING_BASE_URL", "https://kaching.chorus.services") KACHING_SERVICE_TOKEN = os.getenv("KACHING_SERVICE_TOKEN", "") # License tier configuration for WHOOSH features LICENSE_TIER_CONFIG = { "evaluation": { "display_name": "Evaluation", "max_search_results": 50, "max_api_calls_per_hour": 100, "max_storage_gb": 1, "features": ["basic-search", "basic-analytics"], "color": "gray" }, "standard": { "display_name": "Standard", "max_search_results": 1000, "max_api_calls_per_hour": 1000, "max_storage_gb": 10, "features": ["basic-search", "advanced-search", "analytics", "workflows"], "color": "blue" }, "enterprise": { "display_name": "Enterprise", "max_search_results": -1, # unlimited "max_api_calls_per_hour": -1, # unlimited "max_storage_gb": 100, "features": ["basic-search", "advanced-search", "analytics", "workflows", "bulk-operations", "enterprise-support", "api-access"], "color": "purple" } } # Pydantic models for license responses class LicenseQuota(BaseModel): """Represents a single quota with usage and limit""" used: int limit: int percentage: float class LicenseQuotas(BaseModel): """All quotas for a license""" search_requests: LicenseQuota storage_gb: LicenseQuota api_calls: LicenseQuota class UpgradeSuggestion(BaseModel): """Upgrade suggestion based on usage patterns""" reason: str current_tier: str suggested_tier: str benefits: List[str] roi_estimate: Optional[str] = None urgency: str # 'low', 'medium', 'high' class LicenseStatus(BaseModel): """Complete license status for a user""" status: str # 'active', 'suspended', 'expired', 'cancelled' tier: str tier_display_name: str features: List[str] max_nodes: int expires_at: str quotas: LicenseQuotas upgrade_suggestions: List[UpgradeSuggestion] tier_color: str class FeatureAvailability(BaseModel): """Feature availability check response""" feature: str available: bool tier_required: Optional[str] = None reason: Optional[str] = None # Helper functions async def resolve_license_id_for_user(user_id: str, db: Session) -> Optional[str]: """ Resolve the license ID for a user based on their organization. In production, this would query the organization/license mapping. For now, we'll use a simple mapping based on user properties. Business Logic: - Each organization has one license - Users inherit license from their organization - Superusers get enterprise tier by default - Regular users get evaluation tier by default """ user = db.query(User).filter(User.id == user_id).first() if not user: return None # TODO: Replace with actual org->license mapping query # For now, use user properties to simulate license assignment if user.is_superuser: return f"enterprise-{user_id}" else: return f"evaluation-{user_id}" async def fetch_license_from_kaching(license_id: str) -> Optional[Dict]: """ Fetch license data from KACHING service. This implements the secure backend proxy pattern. Security Model: - Service-to-service authentication with KACHING - License IDs never exposed to frontend - All license validation happens server-side """ if not KACHING_SERVICE_TOKEN: logger.warning("KACHING_SERVICE_TOKEN not configured - using mock data") return generate_mock_license_data(license_id) try: async with httpx.AsyncClient() as client: response = await client.get( f"{KACHING_BASE_URL}/v1/license/status/{license_id}", headers={"Authorization": f"Bearer {KACHING_SERVICE_TOKEN}"}, timeout=10.0 ) if response.status_code == 200: return response.json() else: logger.error(f"KACHING API error: {response.status_code} - {response.text}") return None except httpx.TimeoutException: logger.error("KACHING API timeout") return None except Exception as e: logger.error(f"Error fetching license from KACHING: {e}") return None def generate_mock_license_data(license_id: str) -> Dict: """ Generate mock license data for development/testing. This simulates KACHING responses during development. """ # Determine tier from license_id prefix if license_id.startswith("enterprise"): tier = "enterprise" elif license_id.startswith("standard"): tier = "standard" else: tier = "evaluation" tier_config = LICENSE_TIER_CONFIG[tier] # Generate mock usage data base_usage = { "evaluation": {"search": 25, "storage": 0.5, "api": 50}, "standard": {"search": 750, "storage": 8, "api": 800}, "enterprise": {"search": 5000, "storage": 45, "api": 2000} } usage = base_usage.get(tier, base_usage["evaluation"]) return { "license_id": license_id, "status": "active", "tier": tier, "expires_at": (datetime.utcnow() + timedelta(days=30)).isoformat(), "max_nodes": 10 if tier == "enterprise" else 3 if tier == "standard" else 1, "quotas": { "search_requests": { "used": usage["search"], "limit": tier_config["max_search_results"] if tier_config["max_search_results"] > 0 else 10000 }, "storage_gb": { "used": int(usage["storage"]), "limit": tier_config["max_storage_gb"] }, "api_calls": { "used": usage["api"], "limit": tier_config["max_api_calls_per_hour"] if tier_config["max_api_calls_per_hour"] > 0 else 5000 } } } def calculate_upgrade_suggestions(tier: str, quotas_data: Dict) -> List[UpgradeSuggestion]: """ Generate intelligent upgrade suggestions based on usage patterns. This implements the revenue optimization logic. Business Intelligence: - High usage triggers upgrade suggestions - Cost-benefit analysis for ROI estimates - Urgency based on proximity to limits """ suggestions = [] if tier == "evaluation": # Always suggest Standard for evaluation users search_usage = quotas_data["search_requests"]["used"] / max(quotas_data["search_requests"]["limit"], 1) if search_usage > 0.8: urgency = "high" reason = "You're approaching your search limit" elif search_usage > 0.5: urgency = "medium" reason = "Increased search capacity recommended" else: urgency = "low" reason = "Unlock advanced features" suggestions.append(UpgradeSuggestion( reason=reason, current_tier="Evaluation", suggested_tier="Standard", benefits=[ "20x more search results (1,000 vs 50)", "Advanced search filters and operators", "Workflow orchestration capabilities", "Analytics dashboard access", "10GB storage (vs 1GB)" ], roi_estimate="Save 15+ hours/month with advanced search", urgency=urgency )) elif tier == "standard": # Check if enterprise features would be beneficial search_usage = quotas_data["search_requests"]["used"] / max(quotas_data["search_requests"]["limit"], 1) api_usage = quotas_data["api_calls"]["used"] / max(quotas_data["api_calls"]["limit"], 1) if search_usage > 0.9 or api_usage > 0.9: urgency = "high" reason = "You're hitting capacity limits regularly" elif search_usage > 0.7 or api_usage > 0.7: urgency = "medium" reason = "Scale your operations with unlimited access" else: return suggestions # No upgrade needed suggestions.append(UpgradeSuggestion( reason=reason, current_tier="Standard", suggested_tier="Enterprise", benefits=[ "Unlimited search results and API calls", "Bulk operations for large datasets", "Priority support and SLA", "Advanced enterprise integrations", "100GB storage capacity" ], roi_estimate="3x productivity increase with unlimited access", urgency=urgency )) return suggestions # API Endpoints @router.get("/license/status", response_model=LicenseStatus) async def get_license_status( current_user: Dict[str, Any] = Depends(get_current_active_user), db: Session = Depends(get_db) ): """ Get current user's license status, tier, and quotas. This endpoint implements the secure proxy pattern: 1. Resolves user's organization to license ID server-side 2. Fetches license data from KACHING (or mock for development) 3. Calculates upgrade suggestions based on usage 4. Returns license information without exposing sensitive IDs Business Value: - Users understand their current tier and limitations - Usage visibility drives upgrade decisions - Proactive suggestions increase conversion rates """ try: user_id = current_user["user_id"] # Resolve license ID for user (server-side only) license_id = await resolve_license_id_for_user(user_id, db) if not license_id: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="No license found for user organization" ) # Fetch license data from KACHING license_data = await fetch_license_from_kaching(license_id) if not license_data: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unable to fetch license information" ) # Extract tier information tier = license_data["tier"] tier_config = LICENSE_TIER_CONFIG.get(tier, LICENSE_TIER_CONFIG["evaluation"]) # Build quota information with usage percentages quotas_data = license_data["quotas"] quotas = LicenseQuotas( search_requests=LicenseQuota( used=quotas_data["search_requests"]["used"], limit=quotas_data["search_requests"]["limit"], percentage=round((quotas_data["search_requests"]["used"] / max(quotas_data["search_requests"]["limit"], 1)) * 100, 1) ), storage_gb=LicenseQuota( used=quotas_data["storage_gb"]["used"], limit=quotas_data["storage_gb"]["limit"], percentage=round((quotas_data["storage_gb"]["used"] / max(quotas_data["storage_gb"]["limit"], 1)) * 100, 1) ), api_calls=LicenseQuota( used=quotas_data["api_calls"]["used"], limit=quotas_data["api_calls"]["limit"], percentage=round((quotas_data["api_calls"]["used"] / max(quotas_data["api_calls"]["limit"], 1)) * 100, 1) ) ) # Generate upgrade suggestions upgrade_suggestions = calculate_upgrade_suggestions(tier, quotas_data) return LicenseStatus( status=license_data["status"], tier=tier, tier_display_name=tier_config["display_name"], features=tier_config["features"], max_nodes=license_data["max_nodes"], expires_at=license_data["expires_at"], quotas=quotas, upgrade_suggestions=upgrade_suggestions, tier_color=tier_config["color"] ) except HTTPException: raise except Exception as e: logger.error(f"Error fetching license status: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Internal server error while fetching license status" ) @router.get("/license/features/{feature_name}", response_model=FeatureAvailability) async def check_feature_availability( feature_name: str, current_user: Dict[str, Any] = Depends(get_current_active_user), db: Session = Depends(get_db) ): """ Check if a specific feature is available to the current user. This endpoint enables feature gating throughout the application: - Server-side feature availability checks prevent client-side bypass - Returns detailed information for user education - Suggests upgrade path if feature is not available Revenue Optimization: - Clear messaging about feature availability - Upgrade path guidance increases conversion - Prevents user frustration with clear explanations """ try: user_id = current_user["user_id"] # Get user's license status license_id = await resolve_license_id_for_user(user_id, db) if not license_id: return FeatureAvailability( feature=feature_name, available=False, reason="No license found" ) license_data = await fetch_license_from_kaching(license_id) if not license_data: return FeatureAvailability( feature=feature_name, available=False, reason="Unable to verify license" ) tier = license_data["tier"] tier_config = LICENSE_TIER_CONFIG.get(tier, LICENSE_TIER_CONFIG["evaluation"]) # Check feature availability available = feature_name in tier_config["features"] if available: return FeatureAvailability( feature=feature_name, available=True ) else: # Find which tier includes this feature required_tier = None for tier_name, config in LICENSE_TIER_CONFIG.items(): if feature_name in config["features"]: required_tier = config["display_name"] break reason = f"Feature requires {required_tier} tier" if required_tier else "Feature not available in any tier" return FeatureAvailability( feature=feature_name, available=False, tier_required=required_tier, reason=reason ) except Exception as e: logger.error(f"Error checking feature availability: {e}") return FeatureAvailability( feature=feature_name, available=False, reason="Error checking feature availability" ) @router.get("/license/quotas", response_model=LicenseQuotas) async def get_license_quotas( current_user: Dict[str, Any] = Depends(get_current_active_user), db: Session = Depends(get_db) ): """ Get detailed quota usage information for the current user. This endpoint supports quota monitoring and alerts: - Real-time usage tracking - Percentage calculations for UI progress bars - Trend analysis for upgrade suggestions User Experience: - Transparent usage visibility builds trust - Proactive limit warnings prevent service disruption - Usage trends justify upgrade investments """ try: user_id = current_user["user_id"] license_id = await resolve_license_id_for_user(user_id, db) if not license_id: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="No license found for user" ) license_data = await fetch_license_from_kaching(license_id) if not license_data: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unable to fetch quota information" ) quotas_data = license_data["quotas"] return LicenseQuotas( search_requests=LicenseQuota( used=quotas_data["search_requests"]["used"], limit=quotas_data["search_requests"]["limit"], percentage=round((quotas_data["search_requests"]["used"] / max(quotas_data["search_requests"]["limit"], 1)) * 100, 1) ), storage_gb=LicenseQuota( used=quotas_data["storage_gb"]["used"], limit=quotas_data["storage_gb"]["limit"], percentage=round((quotas_data["storage_gb"]["used"] / max(quotas_data["storage_gb"]["limit"], 1)) * 100, 1) ), api_calls=LicenseQuota( used=quotas_data["api_calls"]["used"], limit=quotas_data["api_calls"]["limit"], percentage=round((quotas_data["api_calls"]["used"] / max(quotas_data["api_calls"]["limit"], 1)) * 100, 1) ) ) except HTTPException: raise except Exception as e: logger.error(f"Error fetching quotas: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Internal server error while fetching quotas" ) @router.get("/license/upgrade-suggestions", response_model=List[UpgradeSuggestion]) async def get_upgrade_suggestions( current_user: Dict[str, Any] = Depends(get_current_active_user), db: Session = Depends(get_db) ): """ Get personalized upgrade suggestions based on usage patterns. This endpoint implements the revenue optimization engine: - Analyzes usage patterns to identify upgrade opportunities - Calculates ROI estimates for upgrade justification - Prioritizes suggestions by urgency and business impact Business Intelligence: - Data-driven upgrade recommendations - Personalized messaging increases conversion - ROI calculations justify upgrade costs """ try: user_id = current_user["user_id"] license_id = await resolve_license_id_for_user(user_id, db) if not license_id: return [] license_data = await fetch_license_from_kaching(license_id) if not license_data: return [] tier = license_data["tier"] quotas_data = license_data["quotas"] return calculate_upgrade_suggestions(tier, quotas_data) except Exception as e: logger.error(f"Error generating upgrade suggestions: {e}") return [] @router.get("/license/tiers") async def get_available_tiers(): """ Get information about all available license tiers. This endpoint supports the upgrade flow by providing: - Tier comparison information - Feature matrices for decision making - Pricing and capability information Sales Support: - Transparent tier information builds trust - Feature comparisons highlight upgrade benefits - Self-service upgrade path reduces sales friction """ return { "tiers": { tier_name: { "display_name": config["display_name"], "features": config["features"], "max_search_results": config["max_search_results"], "max_storage_gb": config["max_storage_gb"], "color": config["color"] } for tier_name, config in LICENSE_TIER_CONFIG.items() } }