Improve the frontend and repository layer
This commit is contained in:
@@ -8,13 +8,14 @@ from typing import Dict, Any, Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from fastapi import HTTPException, status
|
||||
from datetime import datetime, timezone
|
||||
import httpx
|
||||
|
||||
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
|
||||
from shared.subscription.plans import SubscriptionPlanMetadata, get_training_job_quota, get_forecast_quota
|
||||
from shared.clients.recipes_client import create_recipes_client
|
||||
from shared.clients.suppliers_client import create_suppliers_client
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
@@ -459,50 +460,64 @@ class SubscriptionLimitService:
|
||||
return 0
|
||||
|
||||
async def _get_recipe_count(self, tenant_id: str) -> int:
|
||||
"""Get recipe count from recipes service"""
|
||||
"""Get recipe count from recipes service using shared client"""
|
||||
try:
|
||||
from app.core.config import settings
|
||||
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(
|
||||
f"{settings.RECIPES_SERVICE_URL}/api/v1/tenants/{tenant_id}/recipes/count",
|
||||
headers={"X-Internal-Request": "true"}
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
count = data.get("count", 0)
|
||||
# Use the shared recipes client with proper authentication and resilience
|
||||
recipes_client = create_recipes_client(settings)
|
||||
count = await recipes_client.count_recipes(tenant_id)
|
||||
|
||||
logger.info("Retrieved recipe count", tenant_id=tenant_id, count=count)
|
||||
return count
|
||||
logger.info(
|
||||
"Retrieved recipe count via recipes client",
|
||||
tenant_id=tenant_id,
|
||||
count=count
|
||||
)
|
||||
return count
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting recipe count", tenant_id=tenant_id, error=str(e))
|
||||
logger.error(
|
||||
"Error getting recipe count via recipes client",
|
||||
tenant_id=tenant_id,
|
||||
error=str(e)
|
||||
)
|
||||
# Return 0 as fallback to avoid breaking subscription display
|
||||
return 0
|
||||
|
||||
async def _get_supplier_count(self, tenant_id: str) -> int:
|
||||
"""Get supplier count from suppliers service"""
|
||||
"""Get supplier count from suppliers service using shared client"""
|
||||
try:
|
||||
from app.core.config import settings
|
||||
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(
|
||||
f"{settings.SUPPLIERS_SERVICE_URL}/api/v1/tenants/{tenant_id}/suppliers/count",
|
||||
headers={"X-Internal-Request": "true"}
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
count = data.get("count", 0)
|
||||
# Use the shared suppliers client with proper authentication and resilience
|
||||
suppliers_client = create_suppliers_client(settings)
|
||||
count = await suppliers_client.count_suppliers(tenant_id)
|
||||
|
||||
logger.info("Retrieved supplier count", tenant_id=tenant_id, count=count)
|
||||
return count
|
||||
logger.info(
|
||||
"Retrieved supplier count via suppliers client",
|
||||
tenant_id=tenant_id,
|
||||
count=count
|
||||
)
|
||||
return count
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting supplier count", tenant_id=tenant_id, error=str(e))
|
||||
logger.error(
|
||||
"Error getting supplier count via suppliers client",
|
||||
tenant_id=tenant_id,
|
||||
error=str(e)
|
||||
)
|
||||
# Return 0 as fallback to avoid breaking subscription display
|
||||
return 0
|
||||
|
||||
async def _get_redis_quota(self, quota_key: str) -> int:
|
||||
"""Get current count from Redis quota key"""
|
||||
try:
|
||||
if not self.redis:
|
||||
# Try to initialize Redis client if not available
|
||||
from app.core.config import settings
|
||||
import shared.redis_utils
|
||||
self.redis = await shared.redis_utils.initialize_redis(settings.REDIS_URL)
|
||||
|
||||
if not self.redis:
|
||||
return 0
|
||||
|
||||
@@ -607,4 +622,4 @@ class SubscriptionLimitService:
|
||||
"""Get limit value from plan metadata"""
|
||||
plan_metadata = SubscriptionPlanMetadata.PLANS.get(plan, {})
|
||||
limit = plan_metadata.get('limits', {}).get(limit_key)
|
||||
return limit if limit != -1 else None
|
||||
return limit if limit != -1 else None
|
||||
|
||||
262
services/tenant/app/services/tenant_settings_service.py
Normal file
262
services/tenant/app/services/tenant_settings_service.py
Normal file
@@ -0,0 +1,262 @@
|
||||
# services/tenant/app/services/tenant_settings_service.py
|
||||
"""
|
||||
Tenant Settings Service
|
||||
Business logic for managing tenant-specific operational settings
|
||||
"""
|
||||
|
||||
import structlog
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from uuid import UUID
|
||||
from typing import Optional, Dict, Any
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from ..models.tenant_settings import TenantSettings
|
||||
from ..repositories.tenant_settings_repository import TenantSettingsRepository
|
||||
from ..schemas.tenant_settings import (
|
||||
TenantSettingsUpdate,
|
||||
ProcurementSettings,
|
||||
InventorySettings,
|
||||
ProductionSettings,
|
||||
SupplierSettings,
|
||||
POSSettings,
|
||||
OrderSettings
|
||||
)
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class TenantSettingsService:
|
||||
"""
|
||||
Service for managing tenant settings
|
||||
Handles validation, CRUD operations, and default value management
|
||||
"""
|
||||
|
||||
# Map category names to schema validators
|
||||
CATEGORY_SCHEMAS = {
|
||||
"procurement": ProcurementSettings,
|
||||
"inventory": InventorySettings,
|
||||
"production": ProductionSettings,
|
||||
"supplier": SupplierSettings,
|
||||
"pos": POSSettings,
|
||||
"order": OrderSettings
|
||||
}
|
||||
|
||||
# Map category names to database column names
|
||||
CATEGORY_COLUMNS = {
|
||||
"procurement": "procurement_settings",
|
||||
"inventory": "inventory_settings",
|
||||
"production": "production_settings",
|
||||
"supplier": "supplier_settings",
|
||||
"pos": "pos_settings",
|
||||
"order": "order_settings"
|
||||
}
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
self.repository = TenantSettingsRepository(db)
|
||||
|
||||
async def get_settings(self, tenant_id: UUID) -> TenantSettings:
|
||||
"""
|
||||
Get tenant settings, creating defaults if they don't exist
|
||||
|
||||
Args:
|
||||
tenant_id: UUID of the tenant
|
||||
|
||||
Returns:
|
||||
TenantSettings object
|
||||
|
||||
Raises:
|
||||
HTTPException: If tenant not found
|
||||
"""
|
||||
try:
|
||||
# Try to get existing settings using repository
|
||||
settings = await self.repository.get_by_tenant_id(tenant_id)
|
||||
|
||||
logger.info(f"Existing settings lookup for tenant {tenant_id}: {'found' if settings else 'not found'}")
|
||||
|
||||
# Create default settings if they don't exist
|
||||
if not settings:
|
||||
logger.info(f"Creating default settings for tenant {tenant_id}")
|
||||
settings = await self._create_default_settings(tenant_id)
|
||||
logger.info(f"Successfully created default settings for tenant {tenant_id}")
|
||||
|
||||
return settings
|
||||
except Exception as e:
|
||||
logger.error("Failed to get or create tenant settings", tenant_id=tenant_id, error=str(e), exc_info=True)
|
||||
# Re-raise as HTTPException to match the expected behavior
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to get tenant settings: {str(e)}"
|
||||
)
|
||||
|
||||
async def update_settings(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
updates: TenantSettingsUpdate
|
||||
) -> TenantSettings:
|
||||
"""
|
||||
Update tenant settings
|
||||
|
||||
Args:
|
||||
tenant_id: UUID of the tenant
|
||||
updates: TenantSettingsUpdate object with new values
|
||||
|
||||
Returns:
|
||||
Updated TenantSettings object
|
||||
"""
|
||||
settings = await self.get_settings(tenant_id)
|
||||
|
||||
# Update each category if provided
|
||||
if updates.procurement_settings is not None:
|
||||
settings.procurement_settings = updates.procurement_settings.dict()
|
||||
|
||||
if updates.inventory_settings is not None:
|
||||
settings.inventory_settings = updates.inventory_settings.dict()
|
||||
|
||||
if updates.production_settings is not None:
|
||||
settings.production_settings = updates.production_settings.dict()
|
||||
|
||||
if updates.supplier_settings is not None:
|
||||
settings.supplier_settings = updates.supplier_settings.dict()
|
||||
|
||||
if updates.pos_settings is not None:
|
||||
settings.pos_settings = updates.pos_settings.dict()
|
||||
|
||||
if updates.order_settings is not None:
|
||||
settings.order_settings = updates.order_settings.dict()
|
||||
|
||||
return await self.repository.update(settings)
|
||||
|
||||
async def get_category(self, tenant_id: UUID, category: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get settings for a specific category
|
||||
|
||||
Args:
|
||||
tenant_id: UUID of the tenant
|
||||
category: Category name (procurement, inventory, production, etc.)
|
||||
|
||||
Returns:
|
||||
Dictionary with category settings
|
||||
|
||||
Raises:
|
||||
HTTPException: If category is invalid
|
||||
"""
|
||||
if category not in self.CATEGORY_COLUMNS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid category: {category}. Valid categories: {', '.join(self.CATEGORY_COLUMNS.keys())}"
|
||||
)
|
||||
|
||||
settings = await self.get_settings(tenant_id)
|
||||
column_name = self.CATEGORY_COLUMNS[category]
|
||||
|
||||
return getattr(settings, column_name)
|
||||
|
||||
async def update_category(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
category: str,
|
||||
updates: Dict[str, Any]
|
||||
) -> TenantSettings:
|
||||
"""
|
||||
Update settings for a specific category
|
||||
|
||||
Args:
|
||||
tenant_id: UUID of the tenant
|
||||
category: Category name
|
||||
updates: Dictionary with new values
|
||||
|
||||
Returns:
|
||||
Updated TenantSettings object
|
||||
|
||||
Raises:
|
||||
HTTPException: If category is invalid or validation fails
|
||||
"""
|
||||
if category not in self.CATEGORY_COLUMNS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid category: {category}"
|
||||
)
|
||||
|
||||
# Validate updates using the appropriate schema
|
||||
schema = self.CATEGORY_SCHEMAS[category]
|
||||
try:
|
||||
validated_data = schema(**updates)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=f"Validation error: {str(e)}"
|
||||
)
|
||||
|
||||
# Get existing settings and update the category
|
||||
settings = await self.get_settings(tenant_id)
|
||||
column_name = self.CATEGORY_COLUMNS[category]
|
||||
setattr(settings, column_name, validated_data.dict())
|
||||
|
||||
return await self.repository.update(settings)
|
||||
|
||||
async def reset_category(self, tenant_id: UUID, category: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Reset a category to default values
|
||||
|
||||
Args:
|
||||
tenant_id: UUID of the tenant
|
||||
category: Category name
|
||||
|
||||
Returns:
|
||||
Dictionary with reset category settings
|
||||
|
||||
Raises:
|
||||
HTTPException: If category is invalid
|
||||
"""
|
||||
if category not in self.CATEGORY_COLUMNS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid category: {category}"
|
||||
)
|
||||
|
||||
# Get default settings for the category
|
||||
defaults = TenantSettings.get_default_settings()
|
||||
column_name = self.CATEGORY_COLUMNS[category]
|
||||
default_category_settings = defaults[column_name]
|
||||
|
||||
# Update the category with defaults
|
||||
settings = await self.get_settings(tenant_id)
|
||||
setattr(settings, column_name, default_category_settings)
|
||||
|
||||
await self.repository.update(settings)
|
||||
|
||||
return default_category_settings
|
||||
|
||||
async def _create_default_settings(self, tenant_id: UUID) -> TenantSettings:
|
||||
"""
|
||||
Create default settings for a new tenant
|
||||
|
||||
Args:
|
||||
tenant_id: UUID of the tenant
|
||||
|
||||
Returns:
|
||||
Newly created TenantSettings object
|
||||
"""
|
||||
defaults = TenantSettings.get_default_settings()
|
||||
|
||||
settings = TenantSettings(
|
||||
tenant_id=tenant_id,
|
||||
procurement_settings=defaults["procurement_settings"],
|
||||
inventory_settings=defaults["inventory_settings"],
|
||||
production_settings=defaults["production_settings"],
|
||||
supplier_settings=defaults["supplier_settings"],
|
||||
pos_settings=defaults["pos_settings"],
|
||||
order_settings=defaults["order_settings"]
|
||||
)
|
||||
|
||||
return await self.repository.create(settings)
|
||||
|
||||
async def delete_settings(self, tenant_id: UUID) -> None:
|
||||
"""
|
||||
Delete tenant settings (used when tenant is deleted)
|
||||
|
||||
Args:
|
||||
tenant_id: UUID of the tenant
|
||||
"""
|
||||
await self.repository.delete(tenant_id)
|
||||
Reference in New Issue
Block a user