332 lines
16 KiB
Python
332 lines
16 KiB
Python
"""
|
|
Subscription Limit Service
|
|
Service for validating tenant actions against subscription limits and features
|
|
"""
|
|
|
|
import structlog
|
|
from typing import Dict, Any, Optional
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from fastapi import HTTPException, status
|
|
|
|
from app.repositories import SubscriptionRepository, TenantRepository, TenantMemberRepository
|
|
from app.models.tenants import Subscription, Tenant, TenantMember
|
|
from shared.database.exceptions import DatabaseError
|
|
from shared.database.base import create_database_manager
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
|
class SubscriptionLimitService:
|
|
"""Service for validating subscription limits and features"""
|
|
|
|
def __init__(self, database_manager=None):
|
|
self.database_manager = database_manager or create_database_manager()
|
|
|
|
async def _init_repositories(self, session):
|
|
"""Initialize repositories with session"""
|
|
self.subscription_repo = SubscriptionRepository(Subscription, session)
|
|
self.tenant_repo = TenantRepository(Tenant, session)
|
|
self.member_repo = TenantMemberRepository(TenantMember, session)
|
|
return {
|
|
'subscription': self.subscription_repo,
|
|
'tenant': self.tenant_repo,
|
|
'member': self.member_repo
|
|
}
|
|
|
|
async def get_tenant_subscription_limits(self, tenant_id: str) -> Dict[str, Any]:
|
|
"""Get current subscription limits for a tenant"""
|
|
try:
|
|
async with self.database_manager.get_session() as db_session:
|
|
await self._init_repositories(db_session)
|
|
|
|
subscription = await self.subscription_repo.get_active_subscription(tenant_id)
|
|
if not subscription:
|
|
# Return basic limits if no subscription
|
|
return {
|
|
"plan": "starter",
|
|
"max_users": 5,
|
|
"max_locations": 1,
|
|
"max_products": 50,
|
|
"features": {
|
|
"inventory_management": "basic",
|
|
"demand_prediction": "basic",
|
|
"production_reports": "basic",
|
|
"analytics": "basic",
|
|
"support": "email"
|
|
}
|
|
}
|
|
|
|
return {
|
|
"plan": subscription.plan,
|
|
"max_users": subscription.max_users,
|
|
"max_locations": subscription.max_locations,
|
|
"max_products": subscription.max_products,
|
|
"features": subscription.features or {}
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to get subscription limits",
|
|
tenant_id=tenant_id,
|
|
error=str(e))
|
|
# Return basic limits on error
|
|
return {
|
|
"plan": "starter",
|
|
"max_users": 5,
|
|
"max_locations": 1,
|
|
"max_products": 50,
|
|
"features": {}
|
|
}
|
|
|
|
async def can_add_location(self, tenant_id: str) -> Dict[str, Any]:
|
|
"""Check if tenant can add another location"""
|
|
try:
|
|
async with self.database_manager.get_session() as db_session:
|
|
await self._init_repositories(db_session)
|
|
|
|
# Get subscription limits
|
|
subscription = await self.subscription_repo.get_active_subscription(tenant_id)
|
|
if not subscription:
|
|
return {"can_add": False, "reason": "No active subscription"}
|
|
|
|
# Check if unlimited locations (-1)
|
|
if subscription.max_locations == -1:
|
|
return {"can_add": True, "reason": "Unlimited locations allowed"}
|
|
|
|
# Count current locations (this would need to be implemented based on your location model)
|
|
# For now, we'll assume 1 location per tenant as default
|
|
current_locations = 1 # TODO: Implement actual location count
|
|
|
|
can_add = current_locations < subscription.max_locations
|
|
return {
|
|
"can_add": can_add,
|
|
"current_count": current_locations,
|
|
"max_allowed": subscription.max_locations,
|
|
"reason": "Within limits" if can_add else f"Maximum {subscription.max_locations} locations allowed for {subscription.plan} plan"
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to check location limits",
|
|
tenant_id=tenant_id,
|
|
error=str(e))
|
|
return {"can_add": False, "reason": "Error checking limits"}
|
|
|
|
async def can_add_product(self, tenant_id: str) -> Dict[str, Any]:
|
|
"""Check if tenant can add another product"""
|
|
try:
|
|
async with self.database_manager.get_session() as db_session:
|
|
await self._init_repositories(db_session)
|
|
|
|
# Get subscription limits
|
|
subscription = await self.subscription_repo.get_active_subscription(tenant_id)
|
|
if not subscription:
|
|
return {"can_add": False, "reason": "No active subscription"}
|
|
|
|
# Check if unlimited products (-1)
|
|
if subscription.max_products == -1:
|
|
return {"can_add": True, "reason": "Unlimited products allowed"}
|
|
|
|
# Count current products (this would need to be implemented based on your product model)
|
|
# For now, we'll return a placeholder
|
|
current_products = 0 # TODO: Implement actual product count
|
|
|
|
can_add = current_products < subscription.max_products
|
|
return {
|
|
"can_add": can_add,
|
|
"current_count": current_products,
|
|
"max_allowed": subscription.max_products,
|
|
"reason": "Within limits" if can_add else f"Maximum {subscription.max_products} products allowed for {subscription.plan} plan"
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to check product limits",
|
|
tenant_id=tenant_id,
|
|
error=str(e))
|
|
return {"can_add": False, "reason": "Error checking limits"}
|
|
|
|
async def can_add_user(self, tenant_id: str) -> Dict[str, Any]:
|
|
"""Check if tenant can add another user/member"""
|
|
try:
|
|
async with self.database_manager.get_session() as db_session:
|
|
await self._init_repositories(db_session)
|
|
|
|
# Get subscription limits
|
|
subscription = await self.subscription_repo.get_active_subscription(tenant_id)
|
|
if not subscription:
|
|
return {"can_add": False, "reason": "No active subscription"}
|
|
|
|
# Check if unlimited users (-1)
|
|
if subscription.max_users == -1:
|
|
return {"can_add": True, "reason": "Unlimited users allowed"}
|
|
|
|
# Count current active members
|
|
members = await self.member_repo.get_tenant_members(tenant_id, active_only=True)
|
|
current_users = len(members)
|
|
|
|
can_add = current_users < subscription.max_users
|
|
return {
|
|
"can_add": can_add,
|
|
"current_count": current_users,
|
|
"max_allowed": subscription.max_users,
|
|
"reason": "Within limits" if can_add else f"Maximum {subscription.max_users} users allowed for {subscription.plan} plan"
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to check user limits",
|
|
tenant_id=tenant_id,
|
|
error=str(e))
|
|
return {"can_add": False, "reason": "Error checking limits"}
|
|
|
|
async def has_feature(self, tenant_id: str, feature: str) -> Dict[str, Any]:
|
|
"""Check if tenant has access to a specific feature"""
|
|
try:
|
|
async with self.database_manager.get_session() as db_session:
|
|
await self._init_repositories(db_session)
|
|
|
|
subscription = await self.subscription_repo.get_active_subscription(tenant_id)
|
|
if not subscription:
|
|
return {"has_feature": False, "reason": "No active subscription"}
|
|
|
|
features = subscription.features or {}
|
|
has_feature = feature in features
|
|
|
|
return {
|
|
"has_feature": has_feature,
|
|
"feature_value": features.get(feature),
|
|
"plan": subscription.plan,
|
|
"reason": "Feature available" if has_feature else f"Feature '{feature}' not available in {subscription.plan} plan"
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to check feature access",
|
|
tenant_id=tenant_id,
|
|
feature=feature,
|
|
error=str(e))
|
|
return {"has_feature": False, "reason": "Error checking feature access"}
|
|
|
|
async def get_feature_level(self, tenant_id: str, feature: str) -> Optional[str]:
|
|
"""Get the level/type of a feature for a tenant"""
|
|
try:
|
|
async with self.database_manager.get_session() as db_session:
|
|
await self._init_repositories(db_session)
|
|
|
|
subscription = await self.subscription_repo.get_active_subscription(tenant_id)
|
|
if not subscription:
|
|
return None
|
|
|
|
features = subscription.features or {}
|
|
return features.get(feature)
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to get feature level",
|
|
tenant_id=tenant_id,
|
|
feature=feature,
|
|
error=str(e))
|
|
return None
|
|
|
|
async def validate_plan_upgrade(self, tenant_id: str, new_plan: str) -> Dict[str, Any]:
|
|
"""Validate if a tenant can upgrade to a new plan"""
|
|
try:
|
|
async with self.database_manager.get_session() as db_session:
|
|
await self._init_repositories(db_session)
|
|
|
|
# Get current subscription
|
|
current_subscription = await self.subscription_repo.get_active_subscription(tenant_id)
|
|
if not current_subscription:
|
|
return {"can_upgrade": True, "reason": "No current subscription, can start with any plan"}
|
|
|
|
# Define plan hierarchy
|
|
plan_hierarchy = {"starter": 1, "professional": 2, "enterprise": 3}
|
|
|
|
current_level = plan_hierarchy.get(current_subscription.plan, 0)
|
|
new_level = plan_hierarchy.get(new_plan, 0)
|
|
|
|
if new_level == 0:
|
|
return {"can_upgrade": False, "reason": f"Invalid plan: {new_plan}"}
|
|
|
|
# Check current usage against new plan limits
|
|
from app.repositories.subscription_repository import SubscriptionRepository
|
|
temp_repo = SubscriptionRepository(Subscription, db_session)
|
|
new_plan_config = temp_repo._get_plan_configuration(new_plan)
|
|
|
|
# Check if current usage fits new plan
|
|
members = await self.member_repo.get_tenant_members(tenant_id, active_only=True)
|
|
current_users = len(members)
|
|
|
|
if new_plan_config["max_users"] != -1 and current_users > new_plan_config["max_users"]:
|
|
return {
|
|
"can_upgrade": False,
|
|
"reason": f"Current usage ({current_users} users) exceeds {new_plan} plan limits ({new_plan_config['max_users']} users)"
|
|
}
|
|
|
|
return {
|
|
"can_upgrade": True,
|
|
"current_plan": current_subscription.plan,
|
|
"new_plan": new_plan,
|
|
"price_change": new_plan_config["monthly_price"] - current_subscription.monthly_price,
|
|
"new_features": new_plan_config.get("features", {}),
|
|
"reason": "Upgrade validation successful"
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to validate plan upgrade",
|
|
tenant_id=tenant_id,
|
|
new_plan=new_plan,
|
|
error=str(e))
|
|
return {"can_upgrade": False, "reason": "Error validating upgrade"}
|
|
|
|
async def get_usage_summary(self, tenant_id: str) -> Dict[str, Any]:
|
|
"""Get a summary of current usage vs limits for a tenant"""
|
|
try:
|
|
async with self.database_manager.get_session() as db_session:
|
|
await self._init_repositories(db_session)
|
|
|
|
subscription = await self.subscription_repo.get_active_subscription(tenant_id)
|
|
if not subscription:
|
|
return {"error": "No active subscription"}
|
|
|
|
# Get current usage
|
|
members = await self.member_repo.get_tenant_members(tenant_id, active_only=True)
|
|
current_users = len(members)
|
|
|
|
# TODO: Implement actual location and product counts
|
|
current_locations = 1
|
|
current_products = 0
|
|
|
|
return {
|
|
"plan": subscription.plan,
|
|
"monthly_price": subscription.monthly_price,
|
|
"status": subscription.status,
|
|
"usage": {
|
|
"users": {
|
|
"current": current_users,
|
|
"limit": subscription.max_users,
|
|
"unlimited": subscription.max_users == -1,
|
|
"usage_percentage": 0 if subscription.max_users == -1 else (current_users / subscription.max_users) * 100
|
|
},
|
|
"locations": {
|
|
"current": current_locations,
|
|
"limit": subscription.max_locations,
|
|
"unlimited": subscription.max_locations == -1,
|
|
"usage_percentage": 0 if subscription.max_locations == -1 else (current_locations / subscription.max_locations) * 100
|
|
},
|
|
"products": {
|
|
"current": current_products,
|
|
"limit": subscription.max_products,
|
|
"unlimited": subscription.max_products == -1,
|
|
"usage_percentage": 0 if subscription.max_products == -1 else (current_products / subscription.max_products) * 100 if subscription.max_products > 0 else 0
|
|
}
|
|
},
|
|
"features": subscription.features or {},
|
|
"next_billing_date": subscription.next_billing_date.isoformat() if subscription.next_billing_date else None,
|
|
"trial_ends_at": subscription.trial_ends_at.isoformat() if subscription.trial_ends_at else None
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to get usage summary",
|
|
tenant_id=tenant_id,
|
|
error=str(e))
|
|
return {"error": "Failed to get usage summary"}
|
|
|
|
|
|
# Legacy alias for backward compatibility
|
|
SubscriptionService = SubscriptionLimitService |