""" Subscription Plans API Public endpoint for fetching available subscription plans """ from fastapi import APIRouter, HTTPException from typing import Dict, Any import structlog from shared.subscription.plans import ( SubscriptionTier, SubscriptionPlanMetadata, PlanPricing, QuotaLimits, PlanFeatures, FeatureCategories, UserFacingFeatures ) logger = structlog.get_logger() router = APIRouter(prefix="/plans", tags=["subscription-plans"]) @router.get("", response_model=Dict[str, Any]) async def get_available_plans(): """ Get all available subscription plans with complete metadata **Public endpoint** - No authentication required Returns: Dictionary containing plan metadata for all tiers Example Response: ```json { "plans": { "starter": { "name": "Starter", "description": "Perfect for small bakeries getting started", "monthly_price": 49.00, "yearly_price": 490.00, "features": [...], "limits": {...} }, ... } } ``` """ try: plans_data = {} for tier in SubscriptionTier: metadata = SubscriptionPlanMetadata.PLANS[tier] # Convert Decimal to float for JSON serialization plans_data[tier.value] = { "name": metadata["name"], "description_key": metadata["description_key"], "tagline_key": metadata["tagline_key"], "popular": metadata["popular"], "monthly_price": float(metadata["monthly_price"]), "yearly_price": float(metadata["yearly_price"]), "trial_days": metadata["trial_days"], "features": metadata["features"], "hero_features": metadata.get("hero_features", []), "roi_badge": metadata.get("roi_badge"), "business_metrics": metadata.get("business_metrics"), "limits": metadata["limits"], "support_key": metadata["support_key"], "recommended_for_key": metadata["recommended_for_key"], "contact_sales": metadata.get("contact_sales", False), "custom_pricing": metadata.get("custom_pricing", False), } logger.info("subscription_plans_fetched", tier_count=len(plans_data)) return {"plans": plans_data} except Exception as e: logger.error("failed_to_fetch_plans", error=str(e)) raise HTTPException( status_code=500, detail="Failed to fetch subscription plans" ) @router.get("/{tier}", response_model=Dict[str, Any]) async def get_plan_by_tier(tier: str): """ Get metadata for a specific subscription tier **Public endpoint** - No authentication required Args: tier: Subscription tier (starter, professional, enterprise) Returns: Plan metadata for the specified tier Raises: 404: If tier is not found """ try: # Validate tier tier_enum = SubscriptionTier(tier.lower()) metadata = SubscriptionPlanMetadata.PLANS[tier_enum] plan_data = { "tier": tier_enum.value, "name": metadata["name"], "description_key": metadata["description_key"], "tagline_key": metadata["tagline_key"], "popular": metadata["popular"], "monthly_price": float(metadata["monthly_price"]), "yearly_price": float(metadata["yearly_price"]), "trial_days": metadata["trial_days"], "features": metadata["features"], "hero_features": metadata.get("hero_features", []), "roi_badge": metadata.get("roi_badge"), "business_metrics": metadata.get("business_metrics"), "limits": metadata["limits"], "support_key": metadata["support_key"], "recommended_for_key": metadata["recommended_for_key"], "contact_sales": metadata.get("contact_sales", False), "custom_pricing": metadata.get("custom_pricing", False), } logger.info("subscription_plan_fetched", tier=tier) return plan_data except ValueError: raise HTTPException( status_code=404, detail=f"Subscription tier '{tier}' not found" ) except Exception as e: logger.error("failed_to_fetch_plan", tier=tier, error=str(e)) raise HTTPException( status_code=500, detail="Failed to fetch subscription plan" ) @router.get("/{tier}/features") async def get_plan_features(tier: str): """ Get all features available in a subscription tier **Public endpoint** - No authentication required Args: tier: Subscription tier (starter, professional, enterprise) Returns: List of feature keys available in the tier """ try: tier_enum = SubscriptionTier(tier.lower()) features = PlanFeatures.get_features(tier_enum.value) return { "tier": tier_enum.value, "features": features, "feature_count": len(features) } except ValueError: raise HTTPException( status_code=404, detail=f"Subscription tier '{tier}' not found" ) @router.get("/{tier}/limits") async def get_plan_limits(tier: str): """ Get all quota limits for a subscription tier **Public endpoint** - No authentication required Args: tier: Subscription tier (starter, professional, enterprise) Returns: All quota limits for the tier """ try: tier_enum = SubscriptionTier(tier.lower()) limits = { "tier": tier_enum.value, "team_and_organization": { "max_users": QuotaLimits.MAX_USERS[tier_enum], "max_locations": QuotaLimits.MAX_LOCATIONS[tier_enum], }, "product_and_inventory": { "max_products": QuotaLimits.MAX_PRODUCTS[tier_enum], "max_recipes": QuotaLimits.MAX_RECIPES[tier_enum], "max_suppliers": QuotaLimits.MAX_SUPPLIERS[tier_enum], }, "ml_and_analytics": { "training_jobs_per_day": QuotaLimits.TRAINING_JOBS_PER_DAY[tier_enum], "forecast_generation_per_day": QuotaLimits.FORECAST_GENERATION_PER_DAY[tier_enum], "dataset_size_rows": QuotaLimits.DATASET_SIZE_ROWS[tier_enum], "forecast_horizon_days": QuotaLimits.FORECAST_HORIZON_DAYS[tier_enum], "historical_data_access_days": QuotaLimits.HISTORICAL_DATA_ACCESS_DAYS[tier_enum], }, "import_export": { "bulk_import_rows": QuotaLimits.BULK_IMPORT_ROWS[tier_enum], "bulk_export_rows": QuotaLimits.BULK_EXPORT_ROWS[tier_enum], }, "integrations": { "pos_sync_interval_minutes": QuotaLimits.POS_SYNC_INTERVAL_MINUTES[tier_enum], "api_calls_per_hour": QuotaLimits.API_CALLS_PER_HOUR[tier_enum], "webhook_endpoints": QuotaLimits.WEBHOOK_ENDPOINTS[tier_enum], }, "storage": { "file_storage_gb": QuotaLimits.FILE_STORAGE_GB[tier_enum], "report_retention_days": QuotaLimits.REPORT_RETENTION_DAYS[tier_enum], } } return limits except ValueError: raise HTTPException( status_code=404, detail=f"Subscription tier '{tier}' not found" ) @router.get("/feature-categories") async def get_feature_categories(): """ Get all feature categories with icons and translation keys **Public endpoint** - No authentication required Returns: Dictionary of feature categories """ try: return { "categories": FeatureCategories.CATEGORIES } except Exception as e: logger.error("failed_to_fetch_feature_categories", error=str(e)) raise HTTPException( status_code=500, detail="Failed to fetch feature categories" ) @router.get("/feature-descriptions") async def get_feature_descriptions(): """ Get user-facing feature descriptions with translation keys **Public endpoint** - No authentication required Returns: Dictionary of feature descriptions mapped by feature key """ try: return { "features": UserFacingFeatures.FEATURE_DISPLAY } except Exception as e: logger.error("failed_to_fetch_feature_descriptions", error=str(e)) raise HTTPException( status_code=500, detail="Failed to fetch feature descriptions" ) @router.get("/compare") async def compare_plans(): """ Get plan comparison data for all tiers **Public endpoint** - No authentication required Returns: Comparison matrix of all plans with key features and limits """ try: comparison = { "tiers": ["starter", "professional", "enterprise"], "pricing": {}, "key_features": {}, "key_limits": {} } for tier in SubscriptionTier: metadata = SubscriptionPlanMetadata.PLANS[tier] # Pricing comparison["pricing"][tier.value] = { "monthly": float(metadata["monthly_price"]), "yearly": float(metadata["yearly_price"]), "savings_percentage": round( ((float(metadata["monthly_price"]) * 12) - float(metadata["yearly_price"])) / (float(metadata["monthly_price"]) * 12) * 100 ) } # Key features (first 10) comparison["key_features"][tier.value] = metadata["features"][:10] # Key limits comparison["key_limits"][tier.value] = { "users": metadata["limits"]["users"], "locations": metadata["limits"]["locations"], "products": metadata["limits"]["products"], "forecasts_per_day": metadata["limits"]["forecasts_per_day"], "training_jobs_per_day": QuotaLimits.TRAINING_JOBS_PER_DAY[tier], } return comparison except Exception as e: logger.error("failed_to_compare_plans", error=str(e)) raise HTTPException( status_code=500, detail="Failed to generate plan comparison" )