Improve the frontend 2
This commit is contained in:
@@ -13,6 +13,7 @@ import json
|
||||
from .base import TenantBaseRepository
|
||||
from app.models.tenants import Subscription
|
||||
from shared.database.exceptions import DatabaseError, ValidationError, DuplicateRecordError
|
||||
from shared.subscription.plans import SubscriptionPlanMetadata, QuotaLimits, PlanPricing
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
@@ -44,13 +45,30 @@ class SubscriptionRepository(TenantBaseRepository):
|
||||
if existing_subscription:
|
||||
raise DuplicateRecordError(f"Tenant already has an active subscription")
|
||||
|
||||
# Set default values based on plan
|
||||
plan_config = self._get_plan_configuration(subscription_data["plan"])
|
||||
|
||||
# Set defaults from plan config
|
||||
for key, value in plan_config.items():
|
||||
if key not in subscription_data:
|
||||
subscription_data[key] = value
|
||||
# Set default values based on plan from centralized configuration
|
||||
plan = subscription_data["plan"]
|
||||
plan_info = SubscriptionPlanMetadata.get_plan_info(plan)
|
||||
|
||||
# Set defaults from centralized plan configuration
|
||||
if "monthly_price" not in subscription_data:
|
||||
billing_cycle = subscription_data.get("billing_cycle", "monthly")
|
||||
subscription_data["monthly_price"] = float(
|
||||
PlanPricing.get_price(plan, billing_cycle)
|
||||
)
|
||||
|
||||
if "max_users" not in subscription_data:
|
||||
subscription_data["max_users"] = QuotaLimits.get_limit('MAX_USERS', plan) or -1
|
||||
|
||||
if "max_locations" not in subscription_data:
|
||||
subscription_data["max_locations"] = QuotaLimits.get_limit('MAX_LOCATIONS', plan) or -1
|
||||
|
||||
if "max_products" not in subscription_data:
|
||||
subscription_data["max_products"] = QuotaLimits.get_limit('MAX_PRODUCTS', plan) or -1
|
||||
|
||||
if "features" not in subscription_data:
|
||||
subscription_data["features"] = {
|
||||
feature: True for feature in plan_info.get("features", [])
|
||||
}
|
||||
|
||||
# Set default subscription values
|
||||
if "status" not in subscription_data:
|
||||
@@ -129,37 +147,47 @@ class SubscriptionRepository(TenantBaseRepository):
|
||||
async def update_subscription_plan(
|
||||
self,
|
||||
subscription_id: str,
|
||||
new_plan: str
|
||||
new_plan: str,
|
||||
billing_cycle: str = "monthly"
|
||||
) -> Optional[Subscription]:
|
||||
"""Update subscription plan and pricing"""
|
||||
"""Update subscription plan and pricing using centralized configuration"""
|
||||
try:
|
||||
valid_plans = ["starter", "professional", "enterprise"]
|
||||
if new_plan not in valid_plans:
|
||||
raise ValidationError(f"Invalid plan. Must be one of: {valid_plans}")
|
||||
|
||||
# Get new plan configuration
|
||||
plan_config = self._get_plan_configuration(new_plan)
|
||||
|
||||
|
||||
# Get current subscription to find tenant_id for cache invalidation
|
||||
subscription = await self.get_by_id(subscription_id)
|
||||
if not subscription:
|
||||
raise ValidationError(f"Subscription {subscription_id} not found")
|
||||
|
||||
# Get new plan configuration from centralized source
|
||||
plan_info = SubscriptionPlanMetadata.get_plan_info(new_plan)
|
||||
|
||||
# Update subscription with new plan details
|
||||
update_data = {
|
||||
"plan": new_plan,
|
||||
"monthly_price": plan_config["monthly_price"],
|
||||
"max_users": plan_config["max_users"],
|
||||
"max_locations": plan_config["max_locations"],
|
||||
"max_products": plan_config["max_products"],
|
||||
"features": plan_config.get("features", {}),
|
||||
"monthly_price": float(PlanPricing.get_price(new_plan, billing_cycle)),
|
||||
"billing_cycle": billing_cycle,
|
||||
"max_users": QuotaLimits.get_limit('MAX_USERS', new_plan) or -1,
|
||||
"max_locations": QuotaLimits.get_limit('MAX_LOCATIONS', new_plan) or -1,
|
||||
"max_products": QuotaLimits.get_limit('MAX_PRODUCTS', new_plan) or -1,
|
||||
"features": {feature: True for feature in plan_info.get("features", [])},
|
||||
"updated_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
|
||||
updated_subscription = await self.update(subscription_id, update_data)
|
||||
|
||||
|
||||
# Invalidate cache
|
||||
await self._invalidate_cache(str(subscription.tenant_id))
|
||||
|
||||
logger.info("Subscription plan updated",
|
||||
subscription_id=subscription_id,
|
||||
new_plan=new_plan,
|
||||
new_price=plan_config["monthly_price"])
|
||||
|
||||
new_price=update_data["monthly_price"])
|
||||
|
||||
return updated_subscription
|
||||
|
||||
|
||||
except ValidationError:
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -176,19 +204,27 @@ class SubscriptionRepository(TenantBaseRepository):
|
||||
) -> Optional[Subscription]:
|
||||
"""Cancel a subscription"""
|
||||
try:
|
||||
# Get subscription to find tenant_id for cache invalidation
|
||||
subscription = await self.get_by_id(subscription_id)
|
||||
if not subscription:
|
||||
raise ValidationError(f"Subscription {subscription_id} not found")
|
||||
|
||||
update_data = {
|
||||
"status": "cancelled",
|
||||
"updated_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
|
||||
updated_subscription = await self.update(subscription_id, update_data)
|
||||
|
||||
|
||||
# Invalidate cache
|
||||
await self._invalidate_cache(str(subscription.tenant_id))
|
||||
|
||||
logger.info("Subscription cancelled",
|
||||
subscription_id=subscription_id,
|
||||
reason=reason)
|
||||
|
||||
|
||||
return updated_subscription
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to cancel subscription",
|
||||
subscription_id=subscription_id,
|
||||
@@ -202,12 +238,20 @@ class SubscriptionRepository(TenantBaseRepository):
|
||||
) -> Optional[Subscription]:
|
||||
"""Suspend a subscription"""
|
||||
try:
|
||||
# Get subscription to find tenant_id for cache invalidation
|
||||
subscription = await self.get_by_id(subscription_id)
|
||||
if not subscription:
|
||||
raise ValidationError(f"Subscription {subscription_id} not found")
|
||||
|
||||
update_data = {
|
||||
"status": "suspended",
|
||||
"updated_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
updated_subscription = await self.update(subscription_id, update_data)
|
||||
|
||||
# Invalidate cache
|
||||
await self._invalidate_cache(str(subscription.tenant_id))
|
||||
|
||||
logger.info("Subscription suspended",
|
||||
subscription_id=subscription_id,
|
||||
@@ -227,23 +271,31 @@ class SubscriptionRepository(TenantBaseRepository):
|
||||
) -> Optional[Subscription]:
|
||||
"""Reactivate a cancelled or suspended subscription"""
|
||||
try:
|
||||
# Get subscription to find tenant_id for cache invalidation
|
||||
subscription = await self.get_by_id(subscription_id)
|
||||
if not subscription:
|
||||
raise ValidationError(f"Subscription {subscription_id} not found")
|
||||
|
||||
# Reset billing date when reactivating
|
||||
next_billing_date = datetime.utcnow() + timedelta(days=30)
|
||||
|
||||
|
||||
update_data = {
|
||||
"status": "active",
|
||||
"next_billing_date": next_billing_date,
|
||||
"updated_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
|
||||
updated_subscription = await self.update(subscription_id, update_data)
|
||||
|
||||
|
||||
# Invalidate cache
|
||||
await self._invalidate_cache(str(subscription.tenant_id))
|
||||
|
||||
logger.info("Subscription reactivated",
|
||||
subscription_id=subscription_id,
|
||||
next_billing_date=next_billing_date)
|
||||
|
||||
|
||||
return updated_subscription
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to reactivate subscription",
|
||||
subscription_id=subscription_id,
|
||||
@@ -394,63 +446,23 @@ class SubscriptionRepository(TenantBaseRepository):
|
||||
logger.error("Failed to cleanup old subscriptions",
|
||||
error=str(e))
|
||||
raise DatabaseError(f"Cleanup failed: {str(e)}")
|
||||
|
||||
def _get_plan_configuration(self, plan: str) -> Dict[str, Any]:
|
||||
"""Get configuration for a subscription plan"""
|
||||
plan_configs = {
|
||||
"starter": {
|
||||
"monthly_price": 49.0,
|
||||
"max_users": 5, # Reasonable for small bakeries
|
||||
"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",
|
||||
"ai_model_configuration": "basic" # Added AI model configuration for all tiers
|
||||
}
|
||||
},
|
||||
"professional": {
|
||||
"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",
|
||||
"ai_model_configuration": "advanced" # Enhanced AI model configuration for Professional
|
||||
}
|
||||
},
|
||||
"enterprise": {
|
||||
"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",
|
||||
"ai_model_configuration": "enterprise" # Full AI model configuration for Enterprise
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return plan_configs.get(plan, plan_configs["starter"])
|
||||
|
||||
async def _invalidate_cache(self, tenant_id: str) -> None:
|
||||
"""
|
||||
Invalidate subscription cache for a tenant
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant ID
|
||||
"""
|
||||
try:
|
||||
from app.services.subscription_cache import get_subscription_cache_service
|
||||
|
||||
cache_service = get_subscription_cache_service()
|
||||
await cache_service.invalidate_subscription_cache(tenant_id)
|
||||
|
||||
logger.debug("Invalidated subscription cache from repository",
|
||||
tenant_id=tenant_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning("Failed to invalidate cache (non-critical)",
|
||||
tenant_id=tenant_id, error=str(e))
|
||||
|
||||
Reference in New Issue
Block a user