Initial commit - production deployment
This commit is contained in:
123
shared/subscription/coupons.py
Executable file
123
shared/subscription/coupons.py
Executable file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Coupon system for subscription discounts and promotions.
|
||||
Supports trial extensions, percentage discounts, and fixed amount discounts.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class DiscountType(str, Enum):
|
||||
"""Type of discount offered by a coupon"""
|
||||
TRIAL_EXTENSION = "trial_extension" # Extends trial period by X days
|
||||
PERCENTAGE = "percentage" # X% off subscription price
|
||||
FIXED_AMOUNT = "fixed_amount" # Fixed amount off subscription price
|
||||
|
||||
|
||||
@dataclass
|
||||
class Coupon:
|
||||
"""Coupon configuration"""
|
||||
id: str
|
||||
code: str
|
||||
discount_type: DiscountType
|
||||
discount_value: int # Days for trial_extension, percentage for percentage, cents for fixed_amount
|
||||
max_redemptions: Optional[int] # None = unlimited
|
||||
current_redemptions: int
|
||||
valid_from: datetime
|
||||
valid_until: Optional[datetime] # None = no expiry
|
||||
active: bool
|
||||
created_at: datetime
|
||||
extra_data: Optional[dict] = None
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""Check if coupon is currently valid"""
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Check if active
|
||||
if not self.active:
|
||||
return False
|
||||
|
||||
# Check if started
|
||||
if now < self.valid_from:
|
||||
return False
|
||||
|
||||
# Check if expired
|
||||
if self.valid_until and now > self.valid_until:
|
||||
return False
|
||||
|
||||
# Check if max redemptions reached
|
||||
if self.max_redemptions and self.current_redemptions >= self.max_redemptions:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def can_be_redeemed(self) -> tuple[bool, str]:
|
||||
"""
|
||||
Check if coupon can be redeemed.
|
||||
Returns (can_redeem, reason_if_not)
|
||||
"""
|
||||
if not self.active:
|
||||
return False, "Coupon is inactive"
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
if now < self.valid_from:
|
||||
return False, "Coupon is not yet valid"
|
||||
|
||||
if self.valid_until and now > self.valid_until:
|
||||
return False, "Coupon has expired"
|
||||
|
||||
if self.max_redemptions and self.current_redemptions >= self.max_redemptions:
|
||||
return False, "Coupon has reached maximum redemptions"
|
||||
|
||||
return True, ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class CouponRedemption:
|
||||
"""Record of a coupon redemption"""
|
||||
id: str
|
||||
tenant_id: str
|
||||
coupon_code: str
|
||||
redeemed_at: datetime
|
||||
discount_applied: dict # Details of the discount applied
|
||||
extra_data: Optional[dict] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class CouponValidationResult:
|
||||
"""Result of coupon validation"""
|
||||
valid: bool
|
||||
coupon: Optional[Coupon]
|
||||
error_message: Optional[str]
|
||||
discount_preview: Optional[dict] # Preview of discount to be applied
|
||||
|
||||
|
||||
def calculate_trial_end_date(base_trial_days: int, extension_days: int) -> datetime:
|
||||
"""Calculate trial end date with coupon extension"""
|
||||
from datetime import timedelta
|
||||
total_days = base_trial_days + extension_days
|
||||
return datetime.now(timezone.utc) + timedelta(days=total_days)
|
||||
|
||||
|
||||
def format_discount_description(coupon: Coupon) -> str:
|
||||
"""Format human-readable discount description"""
|
||||
if coupon.discount_type == DiscountType.TRIAL_EXTENSION:
|
||||
months = coupon.discount_value // 30
|
||||
days = coupon.discount_value % 30
|
||||
if months > 0 and days == 0:
|
||||
return f"{months} {'mes' if months == 1 else 'meses'} gratis"
|
||||
elif months > 0:
|
||||
return f"{months} {'mes' if months == 1 else 'meses'} y {days} días gratis"
|
||||
else:
|
||||
return f"{coupon.discount_value} días gratis"
|
||||
|
||||
elif coupon.discount_type == DiscountType.PERCENTAGE:
|
||||
return f"{coupon.discount_value}% de descuento"
|
||||
|
||||
elif coupon.discount_type == DiscountType.FIXED_AMOUNT:
|
||||
amount = coupon.discount_value / 100 # Convert cents to euros
|
||||
return f"€{amount:.2f} de descuento"
|
||||
|
||||
return "Descuento aplicado"
|
||||
782
shared/subscription/plans.py
Executable file
782
shared/subscription/plans.py
Executable file
@@ -0,0 +1,782 @@
|
||||
"""
|
||||
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
|
||||
}
|
||||
|
||||
MAX_CHILD_TENANTS = {
|
||||
SubscriptionTier.STARTER: 0,
|
||||
SubscriptionTier.PROFESSIONAL: 0,
|
||||
SubscriptionTier.ENTERPRISE: 50, # Default limit for enterprise tier
|
||||
}
|
||||
|
||||
# ===== 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 & Business Intelligence
|
||||
'advanced_analytics',
|
||||
'custom_reports',
|
||||
'sales_analytics',
|
||||
'supplier_performance',
|
||||
'waste_analysis',
|
||||
'profitability_analysis',
|
||||
'business_analytics', # NEW: Hero feature - Easy-to-understand business reports
|
||||
|
||||
# Enhanced AI & Forecasting
|
||||
'enhanced_ai_model', # NEW: Hero feature - 92% accurate neighborhood-aware AI
|
||||
'weather_data_integration',
|
||||
'traffic_data_integration',
|
||||
'seasonal_patterns',
|
||||
'longer_forecast_horizon',
|
||||
|
||||
# Scenario Planning & Decision Support
|
||||
'scenario_modeling',
|
||||
'what_if_analysis',
|
||||
'what_if_scenarios', # NEW: Hero feature - Test decisions before investing
|
||||
'risk_assessment',
|
||||
|
||||
# Multi-location
|
||||
'multi_location_support',
|
||||
'location_comparison',
|
||||
'inventory_transfer',
|
||||
|
||||
# Advanced Production
|
||||
'batch_scaling',
|
||||
'recipe_feasibility_check',
|
||||
|
||||
# Integration
|
||||
'pos_integration',
|
||||
'accounting_export',
|
||||
'basic_api_access',
|
||||
|
||||
# Support
|
||||
'priority_email_support',
|
||||
'phone_support',
|
||||
]
|
||||
|
||||
# ===== Enterprise Tier Features =====
|
||||
ENTERPRISE_FEATURES = PROFESSIONAL_FEATURES + [
|
||||
# Enterprise AI & Advanced Intelligence
|
||||
'enterprise_ai_model', # NEW: Hero feature - Most advanced AI with custom modeling
|
||||
'advanced_ml_parameters',
|
||||
'model_artifacts_access',
|
||||
'custom_algorithms',
|
||||
|
||||
# Production & Distribution Management
|
||||
'production_distribution', # NEW: Hero feature - Central production → multi-store distribution
|
||||
'centralized_dashboard', # NEW: Hero feature - Single control panel for all operations
|
||||
'multi_tenant_management',
|
||||
'parent_child_tenants', # NEW: Enterprise tier feature - hierarchical tenant model
|
||||
'internal_transfers', # NEW: Internal PO transfers between parent/child
|
||||
'distribution_management', # NEW: Internal transfer management
|
||||
'transfer_pricing', # NEW: Cost-based transfer pricing
|
||||
'centralized_demand_aggregation', # NEW: Aggregate demand from all child tenants
|
||||
'multi_location_dashboard', # NEW: Dashboard spanning multiple locations
|
||||
|
||||
# Advanced Integration
|
||||
'full_api_access',
|
||||
'unlimited_webhooks',
|
||||
'erp_integration',
|
||||
'custom_integrations',
|
||||
|
||||
# Enterprise Security & Compliance
|
||||
'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
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def validate_tenant_access(tier: str, tenant_type: str) -> bool:
|
||||
"""
|
||||
Validate tenant type is allowed for subscription tier
|
||||
|
||||
Args:
|
||||
tier: Subscription tier (starter, professional, enterprise)
|
||||
tenant_type: Tenant type (standalone, parent, child)
|
||||
|
||||
Returns:
|
||||
bool: True if tenant type is allowed for this tier
|
||||
"""
|
||||
tier_enum = SubscriptionTier(tier.lower())
|
||||
|
||||
# Only enterprise can have parent/child hierarchy
|
||||
if tenant_type in ["parent", "child"]:
|
||||
return tier_enum == SubscriptionTier.ENTERPRISE
|
||||
|
||||
# Standalone tenants allowed for all tiers
|
||||
return tenant_type == "standalone"
|
||||
|
||||
@staticmethod
|
||||
def validate_internal_transfers(tier: str) -> bool:
|
||||
"""
|
||||
Check if tier can use internal transfers
|
||||
|
||||
Args:
|
||||
tier: Subscription tier
|
||||
|
||||
Returns:
|
||||
bool: True if tier has access to internal transfers
|
||||
"""
|
||||
return PlanFeatures.has_feature(tier, "internal_transfers")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# FEATURE DISPLAY CONFIGURATION (User-Facing)
|
||||
# ============================================================================
|
||||
|
||||
class FeatureCategories:
|
||||
"""User-friendly feature categorization for pricing display"""
|
||||
|
||||
CATEGORIES = {
|
||||
"daily_operations": {
|
||||
"icon": "🏪",
|
||||
"translation_key": "categories.daily_operations",
|
||||
},
|
||||
"smart_forecasting": {
|
||||
"icon": "🤖",
|
||||
"translation_key": "categories.smart_forecasting",
|
||||
},
|
||||
"smart_ordering": {
|
||||
"icon": "📦",
|
||||
"translation_key": "categories.smart_ordering",
|
||||
},
|
||||
"business_insights": {
|
||||
"icon": "📊",
|
||||
"translation_key": "categories.business_insights",
|
||||
},
|
||||
"multi_location": {
|
||||
"icon": "🏢",
|
||||
"translation_key": "categories.multi_location",
|
||||
},
|
||||
"integrations": {
|
||||
"icon": "🔌",
|
||||
"translation_key": "categories.integrations",
|
||||
},
|
||||
"support": {
|
||||
"icon": "👥",
|
||||
"translation_key": "categories.support",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class UserFacingFeatures:
|
||||
"""User-friendly feature descriptions for non-technical bakery owners"""
|
||||
|
||||
FEATURE_DISPLAY = {
|
||||
# Daily Operations
|
||||
"inventory_management": {
|
||||
"translation_key": "features.inventory_management",
|
||||
"tooltip_key": "features.inventory_management_tooltip",
|
||||
"category": "daily_operations",
|
||||
},
|
||||
"sales_tracking": {
|
||||
"translation_key": "features.sales_tracking",
|
||||
"tooltip_key": "features.sales_tracking_tooltip",
|
||||
"category": "daily_operations",
|
||||
},
|
||||
"basic_recipes": {
|
||||
"translation_key": "features.basic_recipes",
|
||||
"tooltip_key": "features.basic_recipes_tooltip",
|
||||
"category": "daily_operations",
|
||||
},
|
||||
"production_planning": {
|
||||
"translation_key": "features.production_planning",
|
||||
"tooltip_key": "features.production_planning_tooltip",
|
||||
"category": "daily_operations",
|
||||
},
|
||||
|
||||
# Smart Forecasting
|
||||
"basic_forecasting": {
|
||||
"translation_key": "features.basic_forecasting",
|
||||
"tooltip_key": "features.basic_forecasting_tooltip",
|
||||
"category": "smart_forecasting",
|
||||
},
|
||||
"demand_prediction": {
|
||||
"translation_key": "features.demand_prediction",
|
||||
"category": "smart_forecasting",
|
||||
},
|
||||
"seasonal_patterns": {
|
||||
"translation_key": "features.seasonal_patterns",
|
||||
"tooltip_key": "features.seasonal_patterns_tooltip",
|
||||
"category": "smart_forecasting",
|
||||
},
|
||||
"weather_data_integration": {
|
||||
"translation_key": "features.weather_data_integration",
|
||||
"tooltip_key": "features.weather_data_integration_tooltip",
|
||||
"category": "smart_forecasting",
|
||||
},
|
||||
"traffic_data_integration": {
|
||||
"translation_key": "features.traffic_data_integration",
|
||||
"tooltip_key": "features.traffic_data_integration_tooltip",
|
||||
"category": "smart_forecasting",
|
||||
},
|
||||
|
||||
# Smart Ordering
|
||||
"supplier_management": {
|
||||
"translation_key": "features.supplier_management",
|
||||
"tooltip_key": "features.supplier_management_tooltip",
|
||||
"category": "smart_ordering",
|
||||
},
|
||||
"waste_tracking": {
|
||||
"translation_key": "features.waste_tracking",
|
||||
"tooltip_key": "features.waste_tracking_tooltip",
|
||||
"category": "smart_ordering",
|
||||
},
|
||||
"expiry_alerts": {
|
||||
"translation_key": "features.expiry_alerts",
|
||||
"tooltip_key": "features.expiry_alerts_tooltip",
|
||||
"category": "smart_ordering",
|
||||
},
|
||||
|
||||
# Business Insights
|
||||
"basic_reporting": {
|
||||
"translation_key": "features.basic_reporting",
|
||||
"category": "business_insights",
|
||||
},
|
||||
"advanced_analytics": {
|
||||
"translation_key": "features.advanced_analytics",
|
||||
"tooltip_key": "features.advanced_analytics_tooltip",
|
||||
"category": "business_insights",
|
||||
},
|
||||
"profitability_analysis": {
|
||||
"translation_key": "features.profitability_analysis",
|
||||
"category": "business_insights",
|
||||
},
|
||||
|
||||
# Multi-Location
|
||||
"multi_location_support": {
|
||||
"translation_key": "features.multi_location_support",
|
||||
"category": "multi_location",
|
||||
},
|
||||
"inventory_transfer": {
|
||||
"translation_key": "features.inventory_transfer",
|
||||
"category": "multi_location",
|
||||
},
|
||||
"location_comparison": {
|
||||
"translation_key": "features.location_comparison",
|
||||
"category": "multi_location",
|
||||
},
|
||||
|
||||
# Integrations
|
||||
"pos_integration": {
|
||||
"translation_key": "features.pos_integration",
|
||||
"tooltip_key": "features.pos_integration_tooltip",
|
||||
"category": "integrations",
|
||||
},
|
||||
"accounting_export": {
|
||||
"translation_key": "features.accounting_export",
|
||||
"category": "integrations",
|
||||
},
|
||||
"full_api_access": {
|
||||
"translation_key": "features.full_api_access",
|
||||
"category": "integrations",
|
||||
},
|
||||
|
||||
# Support
|
||||
"email_support": {
|
||||
"translation_key": "features.email_support",
|
||||
"category": "support",
|
||||
},
|
||||
"phone_support": {
|
||||
"translation_key": "features.phone_support",
|
||||
"category": "support",
|
||||
},
|
||||
"dedicated_account_manager": {
|
||||
"translation_key": "features.dedicated_account_manager",
|
||||
"category": "support",
|
||||
},
|
||||
"24_7_support": {
|
||||
"translation_key": "features.support_24_7",
|
||||
"category": "support",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SUBSCRIPTION PLAN METADATA
|
||||
# ============================================================================
|
||||
|
||||
class SubscriptionPlanMetadata:
|
||||
"""Complete metadata for each subscription plan"""
|
||||
|
||||
PLANS = {
|
||||
SubscriptionTier.STARTER: {
|
||||
"name": "Starter",
|
||||
"description_key": "plans.starter.description",
|
||||
"tagline_key": "plans.starter.tagline",
|
||||
"popular": False,
|
||||
"monthly_price": PlanPricing.MONTHLY_PRICES[SubscriptionTier.STARTER],
|
||||
"yearly_price": PlanPricing.YEARLY_PRICES[SubscriptionTier.STARTER],
|
||||
"trial_days": 0,
|
||||
"features": PlanFeatures.STARTER_FEATURES,
|
||||
|
||||
# Hero features (displayed prominently)
|
||||
"hero_features": [
|
||||
"basic_forecasting",
|
||||
"waste_tracking",
|
||||
"supplier_management",
|
||||
],
|
||||
|
||||
# ROI & Business Value
|
||||
"roi_badge": {
|
||||
"savings_min": 300,
|
||||
"savings_max": 500,
|
||||
"currency": "EUR",
|
||||
"period": "month",
|
||||
"translation_key": "plans.starter.roi_badge",
|
||||
},
|
||||
"business_metrics": {
|
||||
"waste_reduction": "20-30%",
|
||||
"time_saved_hours_week": "5-8",
|
||||
"stockout_reduction": "85-95%",
|
||||
},
|
||||
|
||||
"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],
|
||||
"forecast_horizon_days": QuotaLimits.FORECAST_HORIZON_DAYS[SubscriptionTier.STARTER],
|
||||
},
|
||||
"support_key": "plans.starter.support",
|
||||
"recommended_for_key": "plans.starter.recommended_for",
|
||||
},
|
||||
SubscriptionTier.PROFESSIONAL: {
|
||||
"name": "Professional",
|
||||
"description_key": "plans.professional.description",
|
||||
"tagline_key": "plans.professional.tagline",
|
||||
"popular": True, # Most popular plan
|
||||
"monthly_price": PlanPricing.MONTHLY_PRICES[SubscriptionTier.PROFESSIONAL],
|
||||
"yearly_price": PlanPricing.YEARLY_PRICES[SubscriptionTier.PROFESSIONAL],
|
||||
"trial_days": 0,
|
||||
"features": PlanFeatures.PROFESSIONAL_FEATURES,
|
||||
|
||||
# Hero features (displayed prominently)
|
||||
"hero_features": [
|
||||
"business_analytics",
|
||||
"enhanced_ai_model",
|
||||
"what_if_scenarios",
|
||||
],
|
||||
|
||||
# ROI & Business Value
|
||||
"roi_badge": {
|
||||
"savings_min": 800,
|
||||
"savings_max": 1200,
|
||||
"currency": "EUR",
|
||||
"period": "month",
|
||||
"payback_days": 5,
|
||||
"translation_key": "plans.professional.roi_badge",
|
||||
},
|
||||
"business_metrics": {
|
||||
"waste_reduction": "30-40%",
|
||||
"time_saved_hours_week": "15",
|
||||
"procurement_cost_savings": "5-15%",
|
||||
"payback_days": 5,
|
||||
},
|
||||
|
||||
"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],
|
||||
"forecast_horizon_days": QuotaLimits.FORECAST_HORIZON_DAYS[SubscriptionTier.PROFESSIONAL],
|
||||
},
|
||||
"support_key": "plans.professional.support",
|
||||
"recommended_for_key": "plans.professional.recommended_for",
|
||||
},
|
||||
SubscriptionTier.ENTERPRISE: {
|
||||
"name": "Enterprise",
|
||||
"description_key": "plans.enterprise.description",
|
||||
"tagline_key": "plans.enterprise.tagline",
|
||||
"popular": False,
|
||||
"monthly_price": PlanPricing.MONTHLY_PRICES[SubscriptionTier.ENTERPRISE],
|
||||
"yearly_price": PlanPricing.YEARLY_PRICES[SubscriptionTier.ENTERPRISE],
|
||||
"trial_days": 0,
|
||||
"features": PlanFeatures.ENTERPRISE_FEATURES,
|
||||
|
||||
# Hero features (displayed prominently)
|
||||
"hero_features": [
|
||||
"production_distribution",
|
||||
"centralized_dashboard",
|
||||
"enterprise_ai_model",
|
||||
],
|
||||
|
||||
# ROI & Business Value
|
||||
"roi_badge": {
|
||||
"translation_key": "plans.enterprise.roi_badge",
|
||||
"custom": True,
|
||||
},
|
||||
"business_metrics": {
|
||||
"waste_reduction": "Custom",
|
||||
"time_saved_hours_week": "Custom",
|
||||
"scale": "Unlimited",
|
||||
},
|
||||
|
||||
"limits": {
|
||||
"users": "Unlimited",
|
||||
"locations": "Unlimited",
|
||||
"products": "Unlimited",
|
||||
"forecasts_per_day": "Unlimited",
|
||||
"forecast_horizon_days": QuotaLimits.FORECAST_HORIZON_DAYS[SubscriptionTier.ENTERPRISE],
|
||||
},
|
||||
"support_key": "plans.enterprise.support",
|
||||
"recommended_for_key": "plans.enterprise.recommended_for",
|
||||
"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