321 lines
12 KiB
Python
321 lines
12 KiB
Python
"""
|
|
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")
|
|
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")
|
|
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")
|
|
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")
|
|
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")
|
|
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}")
|
|
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}")
|
|
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")
|
|
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")
|
|
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"
|
|
) |