Improve subcription support
This commit is contained in:
332
services/tenant/app/services/subscription_limit_service.py
Normal file
332
services/tenant/app/services/subscription_limit_service.py
Normal file
@@ -0,0 +1,332 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user