Improve the frontend and repository layer

This commit is contained in:
Urtzi Alfaro
2025-10-23 07:44:54 +02:00
parent 8d30172483
commit 07c33fa578
112 changed files with 14726 additions and 2733 deletions

View File

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

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