Improve subcription support
This commit is contained in:
330
services/tenant/app/api/subscriptions.py
Normal file
330
services/tenant/app/api/subscriptions.py
Normal file
@@ -0,0 +1,330 @@
|
||||
"""
|
||||
Subscription API endpoints for plan limits and feature validation
|
||||
"""
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Path, Query
|
||||
from typing import List, Dict, Any, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from app.services.subscription_limit_service import SubscriptionLimitService
|
||||
from app.repositories import SubscriptionRepository
|
||||
from app.models.tenants import Subscription
|
||||
from shared.auth.decorators import get_current_user_dep, require_admin_role_dep
|
||||
from shared.database.base import create_database_manager
|
||||
from shared.monitoring.metrics import track_endpoint_metrics
|
||||
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter()
|
||||
|
||||
# Dependency injection for subscription limit service
|
||||
def get_subscription_limit_service():
|
||||
try:
|
||||
from app.core.config import settings
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||
return SubscriptionLimitService(database_manager)
|
||||
except Exception as e:
|
||||
logger.error("Failed to create subscription limit service", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Service initialization failed")
|
||||
|
||||
def get_subscription_repository():
|
||||
try:
|
||||
from app.core.config import settings
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||
# This would need to be properly initialized with session
|
||||
# For now, we'll use the service pattern
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error("Failed to create subscription repository", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Repository initialization failed")
|
||||
|
||||
@router.get("/subscriptions/{tenant_id}/limits")
|
||||
@track_endpoint_metrics("subscription_get_limits")
|
||||
async def get_subscription_limits(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Get current subscription limits for a tenant"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user has access to tenant
|
||||
limits = await limit_service.get_tenant_subscription_limits(str(tenant_id))
|
||||
return limits
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get subscription limits",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get subscription limits"
|
||||
)
|
||||
|
||||
@router.get("/subscriptions/{tenant_id}/usage")
|
||||
@track_endpoint_metrics("subscription_get_usage")
|
||||
async def get_usage_summary(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Get usage summary vs limits for a tenant"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user has access to tenant
|
||||
usage = await limit_service.get_usage_summary(str(tenant_id))
|
||||
return usage
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get usage summary",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get usage summary"
|
||||
)
|
||||
|
||||
@router.get("/subscriptions/{tenant_id}/can-add-location")
|
||||
@track_endpoint_metrics("subscription_check_location_limit")
|
||||
async def can_add_location(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Check if tenant can add another location"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user has access to tenant
|
||||
result = await limit_service.can_add_location(str(tenant_id))
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to check location limits",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to check location limits"
|
||||
)
|
||||
|
||||
@router.get("/subscriptions/{tenant_id}/can-add-product")
|
||||
@track_endpoint_metrics("subscription_check_product_limit")
|
||||
async def can_add_product(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Check if tenant can add another product"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user has access to tenant
|
||||
result = await limit_service.can_add_product(str(tenant_id))
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to check product limits",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to check product limits"
|
||||
)
|
||||
|
||||
@router.get("/subscriptions/{tenant_id}/can-add-user")
|
||||
@track_endpoint_metrics("subscription_check_user_limit")
|
||||
async def can_add_user(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Check if tenant can add another user/member"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user has access to tenant
|
||||
result = await limit_service.can_add_user(str(tenant_id))
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to check user limits",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to check user limits"
|
||||
)
|
||||
|
||||
@router.get("/subscriptions/{tenant_id}/features/{feature}")
|
||||
@track_endpoint_metrics("subscription_check_feature")
|
||||
async def has_feature(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
feature: str = Path(..., description="Feature name"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Check if tenant has access to a specific feature"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user has access to tenant
|
||||
result = await limit_service.has_feature(str(tenant_id), feature)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to check feature access",
|
||||
tenant_id=str(tenant_id),
|
||||
feature=feature,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to check feature access"
|
||||
)
|
||||
|
||||
@router.get("/subscriptions/{tenant_id}/validate-upgrade/{new_plan}")
|
||||
@track_endpoint_metrics("subscription_validate_upgrade")
|
||||
async def validate_plan_upgrade(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
new_plan: str = Path(..., description="New plan name"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Validate if tenant can upgrade to a new plan"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user has admin access to tenant
|
||||
result = await limit_service.validate_plan_upgrade(str(tenant_id), new_plan)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to validate plan upgrade",
|
||||
tenant_id=str(tenant_id),
|
||||
new_plan=new_plan,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to validate plan upgrade"
|
||||
)
|
||||
|
||||
@router.post("/subscriptions/{tenant_id}/upgrade")
|
||||
@track_endpoint_metrics("subscription_upgrade_plan")
|
||||
async def upgrade_subscription_plan(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
new_plan: str = Query(..., description="New plan name"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Upgrade subscription plan for a tenant"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user is owner/admin of tenant
|
||||
|
||||
# First validate the upgrade
|
||||
validation = await limit_service.validate_plan_upgrade(str(tenant_id), new_plan)
|
||||
if not validation.get("can_upgrade", False):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=validation.get("reason", "Cannot upgrade to this plan")
|
||||
)
|
||||
|
||||
# TODO: Implement actual plan upgrade logic
|
||||
# This would involve:
|
||||
# 1. Update subscription in database
|
||||
# 2. Process payment changes
|
||||
# 3. Update billing cycle
|
||||
# 4. Send notifications
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Plan upgrade to {new_plan} initiated",
|
||||
"validation": validation
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Failed to upgrade subscription plan",
|
||||
tenant_id=str(tenant_id),
|
||||
new_plan=new_plan,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to upgrade subscription plan"
|
||||
)
|
||||
|
||||
@router.get("/plans/available")
|
||||
@track_endpoint_metrics("subscription_get_available_plans")
|
||||
async def get_available_plans():
|
||||
"""Get all available subscription plans with features and pricing"""
|
||||
|
||||
try:
|
||||
# This could be moved to a config service or database
|
||||
plans = {
|
||||
"starter": {
|
||||
"name": "Starter",
|
||||
"description": "Ideal para panaderías pequeñas o nuevas",
|
||||
"monthly_price": 49.0,
|
||||
"max_users": 5,
|
||||
"max_locations": 1,
|
||||
"max_products": 50,
|
||||
"features": {
|
||||
"inventory_management": "basic",
|
||||
"demand_prediction": "basic",
|
||||
"production_reports": "basic",
|
||||
"analytics": "basic",
|
||||
"support": "email",
|
||||
"trial_days": 14,
|
||||
"locations": "1_location"
|
||||
},
|
||||
"trial_available": True
|
||||
},
|
||||
"professional": {
|
||||
"name": "Professional",
|
||||
"description": "Ideal para panaderías y cadenas en crecimiento",
|
||||
"monthly_price": 129.0,
|
||||
"max_users": 15,
|
||||
"max_locations": 2,
|
||||
"max_products": -1, # Unlimited
|
||||
"features": {
|
||||
"inventory_management": "advanced",
|
||||
"demand_prediction": "ai_92_percent",
|
||||
"production_management": "complete",
|
||||
"pos_integrated": True,
|
||||
"logistics": "basic",
|
||||
"analytics": "advanced",
|
||||
"support": "priority_24_7",
|
||||
"trial_days": 14,
|
||||
"locations": "1_2_locations"
|
||||
},
|
||||
"trial_available": True,
|
||||
"popular": True
|
||||
},
|
||||
"enterprise": {
|
||||
"name": "Enterprise",
|
||||
"description": "Ideal para cadenas con obradores centrales",
|
||||
"monthly_price": 399.0,
|
||||
"max_users": -1, # Unlimited
|
||||
"max_locations": -1, # Unlimited
|
||||
"max_products": -1, # Unlimited
|
||||
"features": {
|
||||
"inventory_management": "multi_location",
|
||||
"demand_prediction": "ai_personalized",
|
||||
"production_optimization": "capacity",
|
||||
"erp_integration": True,
|
||||
"logistics": "advanced",
|
||||
"analytics": "predictive",
|
||||
"api_access": "personalized",
|
||||
"account_manager": True,
|
||||
"demo": "personalized",
|
||||
"locations": "unlimited_obradores"
|
||||
},
|
||||
"trial_available": False,
|
||||
"contact_sales": True
|
||||
}
|
||||
}
|
||||
|
||||
return {"plans": plans}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get available plans", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get available plans"
|
||||
)
|
||||
@@ -4,7 +4,7 @@ Tenant models for bakery management - FIXED
|
||||
Removed cross-service User relationship to eliminate circular dependencies
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, String, Boolean, DateTime, Float, ForeignKey, Text, Integer
|
||||
from sqlalchemy import Column, String, Boolean, DateTime, Float, ForeignKey, Text, Integer, JSON
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime, timezone
|
||||
@@ -35,7 +35,7 @@ class Tenant(Base):
|
||||
|
||||
# Status
|
||||
is_active = Column(Boolean, default=True)
|
||||
subscription_tier = Column(String(50), default="basic")
|
||||
subscription_tier = Column(String(50), default="starter")
|
||||
|
||||
# ML status
|
||||
model_trained = Column(Boolean, default=False)
|
||||
@@ -92,7 +92,7 @@ class Subscription(Base):
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False)
|
||||
|
||||
plan = Column(String(50), default="basic") # basic, professional, enterprise
|
||||
plan = Column(String(50), default="starter") # starter, professional, enterprise
|
||||
status = Column(String(50), default="active") # active, suspended, cancelled
|
||||
|
||||
# Billing
|
||||
@@ -102,10 +102,13 @@ class Subscription(Base):
|
||||
trial_ends_at = Column(DateTime(timezone=True))
|
||||
|
||||
# Limits
|
||||
max_users = Column(Integer, default=1)
|
||||
max_users = Column(Integer, default=5)
|
||||
max_locations = Column(Integer, default=1)
|
||||
max_products = Column(Integer, default=50)
|
||||
|
||||
# Features - Store plan features as JSON
|
||||
features = Column(JSON)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
|
||||
|
||||
@@ -133,7 +133,7 @@ class SubscriptionRepository(TenantBaseRepository):
|
||||
) -> Optional[Subscription]:
|
||||
"""Update subscription plan and pricing"""
|
||||
try:
|
||||
valid_plans = ["basic", "professional", "enterprise"]
|
||||
valid_plans = ["starter", "professional", "enterprise"]
|
||||
if new_plan not in valid_plans:
|
||||
raise ValidationError(f"Invalid plan. Must be one of: {valid_plans}")
|
||||
|
||||
@@ -147,6 +147,7 @@ class SubscriptionRepository(TenantBaseRepository):
|
||||
"max_users": plan_config["max_users"],
|
||||
"max_locations": plan_config["max_locations"],
|
||||
"max_products": plan_config["max_products"],
|
||||
"features": plan_config.get("features", {}),
|
||||
"updated_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
@@ -397,24 +398,56 @@ class SubscriptionRepository(TenantBaseRepository):
|
||||
def _get_plan_configuration(self, plan: str) -> Dict[str, Any]:
|
||||
"""Get configuration for a subscription plan"""
|
||||
plan_configs = {
|
||||
"basic": {
|
||||
"monthly_price": 29.99,
|
||||
"max_users": 2,
|
||||
"starter": {
|
||||
"monthly_price": 49.0,
|
||||
"max_users": 5, # Reasonable for small bakeries
|
||||
"max_locations": 1,
|
||||
"max_products": 50
|
||||
"max_products": 50,
|
||||
"features": {
|
||||
"inventory_management": "basic",
|
||||
"demand_prediction": "basic",
|
||||
"production_reports": "basic",
|
||||
"analytics": "basic",
|
||||
"support": "email",
|
||||
"trial_days": 14,
|
||||
"locations": "1_location"
|
||||
}
|
||||
},
|
||||
"professional": {
|
||||
"monthly_price": 79.99,
|
||||
"max_users": 10,
|
||||
"max_locations": 3,
|
||||
"max_products": 200
|
||||
"monthly_price": 129.0,
|
||||
"max_users": 15, # Good for growing bakeries
|
||||
"max_locations": 2,
|
||||
"max_products": -1, # Unlimited products
|
||||
"features": {
|
||||
"inventory_management": "advanced",
|
||||
"demand_prediction": "ai_92_percent",
|
||||
"production_management": "complete",
|
||||
"pos_integrated": True,
|
||||
"logistics": "basic",
|
||||
"analytics": "advanced",
|
||||
"support": "priority_24_7",
|
||||
"trial_days": 14,
|
||||
"locations": "1_2_locations"
|
||||
}
|
||||
},
|
||||
"enterprise": {
|
||||
"monthly_price": 199.99,
|
||||
"max_users": 50,
|
||||
"max_locations": 10,
|
||||
"max_products": 1000
|
||||
"monthly_price": 399.0,
|
||||
"max_users": -1, # Unlimited users
|
||||
"max_locations": -1, # Unlimited locations
|
||||
"max_products": -1, # Unlimited products
|
||||
"features": {
|
||||
"inventory_management": "multi_location",
|
||||
"demand_prediction": "ai_personalized",
|
||||
"production_optimization": "capacity",
|
||||
"erp_integration": True,
|
||||
"logistics": "advanced",
|
||||
"analytics": "predictive",
|
||||
"api_access": "personalized",
|
||||
"account_manager": True,
|
||||
"demo": "personalized",
|
||||
"locations": "unlimited_obradores"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return plan_configs.get(plan, plan_configs["basic"])
|
||||
return plan_configs.get(plan, plan_configs["starter"])
|
||||
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
|
||||
@@ -84,10 +84,10 @@ class EnhancedTenantService:
|
||||
|
||||
owner_membership = await member_repo.create_membership(membership_data)
|
||||
|
||||
# Create basic subscription
|
||||
# Create starter subscription
|
||||
subscription_data = {
|
||||
"tenant_id": str(tenant.id),
|
||||
"plan": "basic",
|
||||
"plan": "starter",
|
||||
"status": "active"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user