Improve subcription support

This commit is contained in:
Urtzi Alfaro
2025-09-01 19:21:12 +02:00
parent 72b4f60cf5
commit 6346c4bcb9
18 changed files with 3175 additions and 114 deletions

View 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"
)

View File

@@ -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))

View File

@@ -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"])

View 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

View File

@@ -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"
}