Files
bakery-ia/services/tenant/app/services/tenant_settings_service.py
2025-11-13 16:01:08 +01:00

294 lines
9.7 KiB
Python

# 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,
ReplenishmentSettings,
SafetyStockSettings,
MOQSettings,
SupplierSelectionSettings,
NotificationSettings
)
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,
"replenishment": ReplenishmentSettings,
"safety_stock": SafetyStockSettings,
"moq": MOQSettings,
"supplier_selection": SupplierSelectionSettings,
"notification": NotificationSettings
}
# 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",
"replenishment": "replenishment_settings",
"safety_stock": "safety_stock_settings",
"moq": "moq_settings",
"supplier_selection": "supplier_selection_settings",
"notification": "notification_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()
if updates.replenishment_settings is not None:
settings.replenishment_settings = updates.replenishment_settings.dict()
if updates.safety_stock_settings is not None:
settings.safety_stock_settings = updates.safety_stock_settings.dict()
if updates.moq_settings is not None:
settings.moq_settings = updates.moq_settings.dict()
if updates.supplier_selection_settings is not None:
settings.supplier_selection_settings = updates.supplier_selection_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"],
replenishment_settings=defaults["replenishment_settings"],
safety_stock_settings=defaults["safety_stock_settings"],
moq_settings=defaults["moq_settings"],
supplier_selection_settings=defaults["supplier_selection_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)