Improve the frontend 2

This commit is contained in:
Urtzi Alfaro
2025-10-29 06:58:05 +01:00
parent 858d985c92
commit 36217a2729
98 changed files with 6652 additions and 4230 deletions

View File

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