Files
2025-11-15 15:20:10 +01:00

331 lines
10 KiB
Python

"""
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"
)