Add role-based filtering and imporve code
This commit is contained in:
486
shared/subscription/plans.py
Normal file
486
shared/subscription/plans.py
Normal file
@@ -0,0 +1,486 @@
|
||||
"""
|
||||
Centralized Subscription Plan Configuration
|
||||
Owner: Tenant Service
|
||||
Single source of truth for all subscription tiers, quotas, features, and limits
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any, List
|
||||
from enum import Enum
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class SubscriptionTier(str, Enum):
|
||||
"""Subscription tier enumeration"""
|
||||
STARTER = "starter"
|
||||
PROFESSIONAL = "professional"
|
||||
ENTERPRISE = "enterprise"
|
||||
|
||||
|
||||
class BillingCycle(str, Enum):
|
||||
"""Billing cycle options"""
|
||||
MONTHLY = "monthly"
|
||||
YEARLY = "yearly"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PRICING CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
class PlanPricing:
|
||||
"""Pricing for each subscription tier"""
|
||||
|
||||
MONTHLY_PRICES = {
|
||||
SubscriptionTier.STARTER: Decimal("49.00"),
|
||||
SubscriptionTier.PROFESSIONAL: Decimal("149.00"),
|
||||
SubscriptionTier.ENTERPRISE: Decimal("499.00"), # Base price, custom quotes available
|
||||
}
|
||||
|
||||
YEARLY_PRICES = {
|
||||
SubscriptionTier.STARTER: Decimal("490.00"), # ~17% discount (2 months free)
|
||||
SubscriptionTier.PROFESSIONAL: Decimal("1490.00"), # ~17% discount
|
||||
SubscriptionTier.ENTERPRISE: Decimal("4990.00"), # Base price, custom quotes available
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_price(tier: str, billing_cycle: str = "monthly") -> Decimal:
|
||||
"""Get price for tier and billing cycle"""
|
||||
tier_enum = SubscriptionTier(tier.lower())
|
||||
if billing_cycle == "yearly":
|
||||
return PlanPricing.YEARLY_PRICES[tier_enum]
|
||||
return PlanPricing.MONTHLY_PRICES[tier_enum]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# QUOTA LIMITS CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
class QuotaLimits:
|
||||
"""
|
||||
Resource quotas and limits for each subscription tier
|
||||
None = Unlimited
|
||||
"""
|
||||
|
||||
# ===== Team & Organization Limits =====
|
||||
MAX_USERS = {
|
||||
SubscriptionTier.STARTER: 5,
|
||||
SubscriptionTier.PROFESSIONAL: 20,
|
||||
SubscriptionTier.ENTERPRISE: None, # Unlimited
|
||||
}
|
||||
|
||||
MAX_LOCATIONS = {
|
||||
SubscriptionTier.STARTER: 1,
|
||||
SubscriptionTier.PROFESSIONAL: 3,
|
||||
SubscriptionTier.ENTERPRISE: None, # Unlimited
|
||||
}
|
||||
|
||||
# ===== Product & Inventory Limits =====
|
||||
MAX_PRODUCTS = {
|
||||
SubscriptionTier.STARTER: 50,
|
||||
SubscriptionTier.PROFESSIONAL: 500,
|
||||
SubscriptionTier.ENTERPRISE: None, # Unlimited
|
||||
}
|
||||
|
||||
MAX_RECIPES = {
|
||||
SubscriptionTier.STARTER: 25,
|
||||
SubscriptionTier.PROFESSIONAL: 250,
|
||||
SubscriptionTier.ENTERPRISE: None, # Unlimited
|
||||
}
|
||||
|
||||
MAX_SUPPLIERS = {
|
||||
SubscriptionTier.STARTER: 10,
|
||||
SubscriptionTier.PROFESSIONAL: 100,
|
||||
SubscriptionTier.ENTERPRISE: None, # Unlimited
|
||||
}
|
||||
|
||||
# ===== ML & Analytics Quotas (Daily Limits) =====
|
||||
TRAINING_JOBS_PER_DAY = {
|
||||
SubscriptionTier.STARTER: 1,
|
||||
SubscriptionTier.PROFESSIONAL: 5,
|
||||
SubscriptionTier.ENTERPRISE: None, # Unlimited
|
||||
}
|
||||
|
||||
FORECAST_GENERATION_PER_DAY = {
|
||||
SubscriptionTier.STARTER: 10,
|
||||
SubscriptionTier.PROFESSIONAL: 100,
|
||||
SubscriptionTier.ENTERPRISE: None, # Unlimited
|
||||
}
|
||||
|
||||
# ===== Data Limits =====
|
||||
DATASET_SIZE_ROWS = {
|
||||
SubscriptionTier.STARTER: 1000,
|
||||
SubscriptionTier.PROFESSIONAL: 10000,
|
||||
SubscriptionTier.ENTERPRISE: None, # Unlimited
|
||||
}
|
||||
|
||||
FORECAST_HORIZON_DAYS = {
|
||||
SubscriptionTier.STARTER: 7,
|
||||
SubscriptionTier.PROFESSIONAL: 90,
|
||||
SubscriptionTier.ENTERPRISE: 365,
|
||||
}
|
||||
|
||||
HISTORICAL_DATA_ACCESS_DAYS = {
|
||||
SubscriptionTier.STARTER: 30, # 1 month
|
||||
SubscriptionTier.PROFESSIONAL: 365, # 1 year
|
||||
SubscriptionTier.ENTERPRISE: None, # Unlimited
|
||||
}
|
||||
|
||||
# ===== Import/Export Limits =====
|
||||
BULK_IMPORT_ROWS = {
|
||||
SubscriptionTier.STARTER: 100,
|
||||
SubscriptionTier.PROFESSIONAL: 1000,
|
||||
SubscriptionTier.ENTERPRISE: 10000,
|
||||
}
|
||||
|
||||
BULK_EXPORT_ROWS = {
|
||||
SubscriptionTier.STARTER: 1000,
|
||||
SubscriptionTier.PROFESSIONAL: 10000,
|
||||
SubscriptionTier.ENTERPRISE: None, # Unlimited
|
||||
}
|
||||
|
||||
# ===== Integration Limits =====
|
||||
POS_SYNC_INTERVAL_MINUTES = {
|
||||
SubscriptionTier.STARTER: 60, # Hourly
|
||||
SubscriptionTier.PROFESSIONAL: 15, # Every 15 minutes
|
||||
SubscriptionTier.ENTERPRISE: 5, # Every 5 minutes (near real-time)
|
||||
}
|
||||
|
||||
API_CALLS_PER_HOUR = {
|
||||
SubscriptionTier.STARTER: 100,
|
||||
SubscriptionTier.PROFESSIONAL: 1000,
|
||||
SubscriptionTier.ENTERPRISE: 10000,
|
||||
}
|
||||
|
||||
WEBHOOK_ENDPOINTS = {
|
||||
SubscriptionTier.STARTER: 2,
|
||||
SubscriptionTier.PROFESSIONAL: 10,
|
||||
SubscriptionTier.ENTERPRISE: None, # Unlimited
|
||||
}
|
||||
|
||||
# ===== Storage Limits =====
|
||||
FILE_STORAGE_GB = {
|
||||
SubscriptionTier.STARTER: 1,
|
||||
SubscriptionTier.PROFESSIONAL: 10,
|
||||
SubscriptionTier.ENTERPRISE: 100,
|
||||
}
|
||||
|
||||
REPORT_RETENTION_DAYS = {
|
||||
SubscriptionTier.STARTER: 30,
|
||||
SubscriptionTier.PROFESSIONAL: 180,
|
||||
SubscriptionTier.ENTERPRISE: 365,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_limit(quota_type: str, tier: str) -> Optional[int]:
|
||||
"""
|
||||
Get quota limit for a specific type and tier
|
||||
|
||||
Args:
|
||||
quota_type: Quota type (e.g., 'MAX_USERS')
|
||||
tier: Subscription tier
|
||||
|
||||
Returns:
|
||||
Optional[int]: Limit value or None for unlimited
|
||||
"""
|
||||
tier_enum = SubscriptionTier(tier.lower())
|
||||
|
||||
quota_map = {
|
||||
'MAX_USERS': QuotaLimits.MAX_USERS,
|
||||
'MAX_LOCATIONS': QuotaLimits.MAX_LOCATIONS,
|
||||
'MAX_PRODUCTS': QuotaLimits.MAX_PRODUCTS,
|
||||
'MAX_RECIPES': QuotaLimits.MAX_RECIPES,
|
||||
'MAX_SUPPLIERS': QuotaLimits.MAX_SUPPLIERS,
|
||||
'TRAINING_JOBS_PER_DAY': QuotaLimits.TRAINING_JOBS_PER_DAY,
|
||||
'FORECAST_GENERATION_PER_DAY': QuotaLimits.FORECAST_GENERATION_PER_DAY,
|
||||
'DATASET_SIZE_ROWS': QuotaLimits.DATASET_SIZE_ROWS,
|
||||
'FORECAST_HORIZON_DAYS': QuotaLimits.FORECAST_HORIZON_DAYS,
|
||||
'HISTORICAL_DATA_ACCESS_DAYS': QuotaLimits.HISTORICAL_DATA_ACCESS_DAYS,
|
||||
'BULK_IMPORT_ROWS': QuotaLimits.BULK_IMPORT_ROWS,
|
||||
'BULK_EXPORT_ROWS': QuotaLimits.BULK_EXPORT_ROWS,
|
||||
'POS_SYNC_INTERVAL_MINUTES': QuotaLimits.POS_SYNC_INTERVAL_MINUTES,
|
||||
'API_CALLS_PER_HOUR': QuotaLimits.API_CALLS_PER_HOUR,
|
||||
'WEBHOOK_ENDPOINTS': QuotaLimits.WEBHOOK_ENDPOINTS,
|
||||
'FILE_STORAGE_GB': QuotaLimits.FILE_STORAGE_GB,
|
||||
'REPORT_RETENTION_DAYS': QuotaLimits.REPORT_RETENTION_DAYS,
|
||||
}
|
||||
|
||||
quotas = quota_map.get(quota_type, {})
|
||||
return quotas.get(tier_enum)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# FEATURE ACCESS CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
class PlanFeatures:
|
||||
"""
|
||||
Feature availability by subscription tier
|
||||
Each tier includes all features from lower tiers
|
||||
"""
|
||||
|
||||
# ===== Core Features (All Tiers) =====
|
||||
CORE_FEATURES = [
|
||||
'inventory_management',
|
||||
'sales_tracking',
|
||||
'basic_recipes',
|
||||
'production_planning',
|
||||
'basic_reporting',
|
||||
'mobile_app_access',
|
||||
'email_support',
|
||||
'easy_step_by_step_onboarding', # NEW: Value-add onboarding
|
||||
]
|
||||
|
||||
# ===== Starter Tier Features =====
|
||||
STARTER_FEATURES = CORE_FEATURES + [
|
||||
'basic_forecasting',
|
||||
'demand_prediction',
|
||||
'waste_tracking',
|
||||
'order_management',
|
||||
'customer_management',
|
||||
'supplier_management',
|
||||
'batch_tracking',
|
||||
'expiry_alerts',
|
||||
]
|
||||
|
||||
# ===== Professional Tier Features =====
|
||||
PROFESSIONAL_FEATURES = STARTER_FEATURES + [
|
||||
# Advanced Analytics
|
||||
'advanced_analytics',
|
||||
'custom_reports',
|
||||
'sales_analytics',
|
||||
'supplier_performance',
|
||||
'waste_analysis',
|
||||
'profitability_analysis',
|
||||
|
||||
# External Data Integration
|
||||
'weather_data_integration',
|
||||
'traffic_data_integration',
|
||||
|
||||
# Multi-location
|
||||
'multi_location_support',
|
||||
'location_comparison',
|
||||
'inventory_transfer',
|
||||
|
||||
# Advanced Forecasting
|
||||
'batch_scaling',
|
||||
'recipe_feasibility_check',
|
||||
'seasonal_patterns',
|
||||
'longer_forecast_horizon',
|
||||
|
||||
# Integration
|
||||
'pos_integration',
|
||||
'accounting_export',
|
||||
'basic_api_access',
|
||||
|
||||
# Support
|
||||
'priority_email_support',
|
||||
'phone_support',
|
||||
]
|
||||
|
||||
# ===== Enterprise Tier Features =====
|
||||
ENTERPRISE_FEATURES = PROFESSIONAL_FEATURES + [
|
||||
# Advanced ML & AI
|
||||
'scenario_modeling',
|
||||
'what_if_analysis',
|
||||
'risk_assessment',
|
||||
'advanced_ml_parameters',
|
||||
'model_artifacts_access',
|
||||
'custom_algorithms',
|
||||
|
||||
# Advanced Integration
|
||||
'full_api_access',
|
||||
'unlimited_webhooks',
|
||||
'erp_integration',
|
||||
'custom_integrations',
|
||||
|
||||
# Enterprise Features
|
||||
'multi_tenant_management',
|
||||
'white_label_option',
|
||||
'custom_branding',
|
||||
'sso_saml',
|
||||
'advanced_permissions',
|
||||
'audit_logs_export',
|
||||
'compliance_reports',
|
||||
|
||||
# Advanced Analytics
|
||||
'benchmarking',
|
||||
'competitive_analysis',
|
||||
'market_insights',
|
||||
'predictive_maintenance',
|
||||
|
||||
# Premium Support
|
||||
'dedicated_account_manager',
|
||||
'priority_support',
|
||||
'24_7_support',
|
||||
'custom_training',
|
||||
'onsite_support', # Optional add-on
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_features(tier: str) -> List[str]:
|
||||
"""Get all features for a tier"""
|
||||
tier_enum = SubscriptionTier(tier.lower())
|
||||
|
||||
feature_map = {
|
||||
SubscriptionTier.STARTER: PlanFeatures.STARTER_FEATURES,
|
||||
SubscriptionTier.PROFESSIONAL: PlanFeatures.PROFESSIONAL_FEATURES,
|
||||
SubscriptionTier.ENTERPRISE: PlanFeatures.ENTERPRISE_FEATURES,
|
||||
}
|
||||
|
||||
return feature_map.get(tier_enum, PlanFeatures.CORE_FEATURES)
|
||||
|
||||
@staticmethod
|
||||
def has_feature(tier: str, feature: str) -> bool:
|
||||
"""Check if a tier has access to a feature"""
|
||||
features = PlanFeatures.get_features(tier)
|
||||
return feature in features
|
||||
|
||||
@staticmethod
|
||||
def requires_professional_tier(feature: str) -> bool:
|
||||
"""Check if feature requires Professional+ tier"""
|
||||
return (
|
||||
feature not in PlanFeatures.STARTER_FEATURES and
|
||||
feature in PlanFeatures.PROFESSIONAL_FEATURES
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def requires_enterprise_tier(feature: str) -> bool:
|
||||
"""Check if feature requires Enterprise tier"""
|
||||
return (
|
||||
feature not in PlanFeatures.PROFESSIONAL_FEATURES and
|
||||
feature in PlanFeatures.ENTERPRISE_FEATURES
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SUBSCRIPTION PLAN METADATA
|
||||
# ============================================================================
|
||||
|
||||
class SubscriptionPlanMetadata:
|
||||
"""Complete metadata for each subscription plan"""
|
||||
|
||||
PLANS = {
|
||||
SubscriptionTier.STARTER: {
|
||||
"name": "Starter",
|
||||
"description": "Perfect for small bakeries getting started",
|
||||
"tagline": "Essential tools for small operations",
|
||||
"popular": False,
|
||||
"monthly_price": PlanPricing.MONTHLY_PRICES[SubscriptionTier.STARTER],
|
||||
"yearly_price": PlanPricing.YEARLY_PRICES[SubscriptionTier.STARTER],
|
||||
"trial_days": 14,
|
||||
"features": PlanFeatures.STARTER_FEATURES,
|
||||
"limits": {
|
||||
"users": QuotaLimits.MAX_USERS[SubscriptionTier.STARTER],
|
||||
"locations": QuotaLimits.MAX_LOCATIONS[SubscriptionTier.STARTER],
|
||||
"products": QuotaLimits.MAX_PRODUCTS[SubscriptionTier.STARTER],
|
||||
"forecasts_per_day": QuotaLimits.FORECAST_GENERATION_PER_DAY[SubscriptionTier.STARTER],
|
||||
},
|
||||
"support": "Email support (48h response)",
|
||||
"recommended_for": "Single location, up to 5 team members",
|
||||
},
|
||||
SubscriptionTier.PROFESSIONAL: {
|
||||
"name": "Professional",
|
||||
"description": "For growing bakeries with multiple locations",
|
||||
"tagline": "Advanced features & analytics",
|
||||
"popular": True, # Most popular plan
|
||||
"monthly_price": PlanPricing.MONTHLY_PRICES[SubscriptionTier.PROFESSIONAL],
|
||||
"yearly_price": PlanPricing.YEARLY_PRICES[SubscriptionTier.PROFESSIONAL],
|
||||
"trial_days": 14,
|
||||
"features": PlanFeatures.PROFESSIONAL_FEATURES,
|
||||
"limits": {
|
||||
"users": QuotaLimits.MAX_USERS[SubscriptionTier.PROFESSIONAL],
|
||||
"locations": QuotaLimits.MAX_LOCATIONS[SubscriptionTier.PROFESSIONAL],
|
||||
"products": QuotaLimits.MAX_PRODUCTS[SubscriptionTier.PROFESSIONAL],
|
||||
"forecasts_per_day": QuotaLimits.FORECAST_GENERATION_PER_DAY[SubscriptionTier.PROFESSIONAL],
|
||||
},
|
||||
"support": "Priority email + phone support (24h response)",
|
||||
"recommended_for": "Multi-location operations, up to 20 team members",
|
||||
},
|
||||
SubscriptionTier.ENTERPRISE: {
|
||||
"name": "Enterprise",
|
||||
"description": "For large bakery chains and franchises",
|
||||
"tagline": "Unlimited scale & custom solutions",
|
||||
"popular": False,
|
||||
"monthly_price": PlanPricing.MONTHLY_PRICES[SubscriptionTier.ENTERPRISE],
|
||||
"yearly_price": PlanPricing.YEARLY_PRICES[SubscriptionTier.ENTERPRISE],
|
||||
"trial_days": 30,
|
||||
"features": PlanFeatures.ENTERPRISE_FEATURES,
|
||||
"limits": {
|
||||
"users": "Unlimited",
|
||||
"locations": "Unlimited",
|
||||
"products": "Unlimited",
|
||||
"forecasts_per_day": "Unlimited",
|
||||
},
|
||||
"support": "24/7 dedicated support + account manager",
|
||||
"recommended_for": "Enterprise operations, unlimited scale",
|
||||
"custom_pricing": True,
|
||||
"contact_sales": True,
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_plan_info(tier: str) -> Dict[str, Any]:
|
||||
"""Get complete plan information"""
|
||||
tier_enum = SubscriptionTier(tier.lower())
|
||||
return SubscriptionPlanMetadata.PLANS.get(tier_enum, {})
|
||||
|
||||
@staticmethod
|
||||
def get_all_plans() -> Dict[SubscriptionTier, Dict[str, Any]]:
|
||||
"""Get information for all plans"""
|
||||
return SubscriptionPlanMetadata.PLANS
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
def get_training_job_quota(tier: str) -> Optional[int]:
|
||||
"""Get training job daily quota for tier"""
|
||||
return QuotaLimits.get_limit('TRAINING_JOBS_PER_DAY', tier)
|
||||
|
||||
|
||||
def get_forecast_quota(tier: str) -> Optional[int]:
|
||||
"""Get forecast generation daily quota for tier"""
|
||||
return QuotaLimits.get_limit('FORECAST_GENERATION_PER_DAY', tier)
|
||||
|
||||
|
||||
def get_dataset_size_limit(tier: str) -> Optional[int]:
|
||||
"""Get dataset size limit for tier"""
|
||||
return QuotaLimits.get_limit('DATASET_SIZE_ROWS', tier)
|
||||
|
||||
|
||||
def get_forecast_horizon_limit(tier: str) -> int:
|
||||
"""Get forecast horizon limit for tier"""
|
||||
return QuotaLimits.get_limit('FORECAST_HORIZON_DAYS', tier) or 7
|
||||
|
||||
|
||||
def get_historical_data_limit(tier: str) -> Optional[int]:
|
||||
"""Get historical data access limit for tier"""
|
||||
return QuotaLimits.get_limit('HISTORICAL_DATA_ACCESS_DAYS', tier)
|
||||
|
||||
|
||||
def can_access_feature(tier: str, feature: str) -> bool:
|
||||
"""Check if tier can access a feature"""
|
||||
return PlanFeatures.has_feature(tier, feature)
|
||||
|
||||
|
||||
def get_tier_comparison() -> Dict[str, Any]:
|
||||
"""
|
||||
Get feature comparison across all tiers
|
||||
Useful for pricing pages
|
||||
"""
|
||||
return {
|
||||
"tiers": ["starter", "professional", "enterprise"],
|
||||
"features": {
|
||||
"core": PlanFeatures.CORE_FEATURES,
|
||||
"starter_only": list(set(PlanFeatures.STARTER_FEATURES) - set(PlanFeatures.CORE_FEATURES)),
|
||||
"professional_only": list(set(PlanFeatures.PROFESSIONAL_FEATURES) - set(PlanFeatures.STARTER_FEATURES)),
|
||||
"enterprise_only": list(set(PlanFeatures.ENTERPRISE_FEATURES) - set(PlanFeatures.PROFESSIONAL_FEATURES)),
|
||||
},
|
||||
"pricing": {
|
||||
tier.value: {
|
||||
"monthly": float(PlanPricing.MONTHLY_PRICES[tier]),
|
||||
"yearly": float(PlanPricing.YEARLY_PRICES[tier]),
|
||||
}
|
||||
for tier in SubscriptionTier
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user