New alert service
This commit is contained in:
227
services/tenant/app/api/tenant_hierarchy.py
Normal file
227
services/tenant/app/api/tenant_hierarchy.py
Normal file
@@ -0,0 +1,227 @@
|
||||
"""
|
||||
Tenant Hierarchy API - Handles parent-child tenant relationships
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Path
|
||||
from typing import List, Dict, Any
|
||||
from uuid import UUID
|
||||
|
||||
from app.schemas.tenants import TenantResponse
|
||||
from app.services.tenant_service import EnhancedTenantService
|
||||
from app.repositories.tenant_repository import TenantRepository
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.routing.route_builder import RouteBuilder
|
||||
from shared.database.base import create_database_manager
|
||||
from shared.monitoring.metrics import track_endpoint_metrics
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter()
|
||||
route_builder = RouteBuilder("tenants")
|
||||
|
||||
|
||||
# Dependency injection for enhanced tenant service
|
||||
def get_enhanced_tenant_service():
|
||||
try:
|
||||
from app.core.config import settings
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||
return EnhancedTenantService(database_manager)
|
||||
except Exception as e:
|
||||
logger.error("Failed to create enhanced tenant service", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Service initialization failed")
|
||||
|
||||
|
||||
@router.get(route_builder.build_base_route("{tenant_id}/children", include_tenant_prefix=False), response_model=List[TenantResponse])
|
||||
@track_endpoint_metrics("tenant_children_list")
|
||||
async def get_tenant_children(
|
||||
tenant_id: UUID = Path(..., description="Parent Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""
|
||||
Get all child tenants for a parent tenant.
|
||||
This endpoint returns all active child tenants associated with the specified parent tenant.
|
||||
"""
|
||||
try:
|
||||
logger.info(
|
||||
"Get tenant children request received",
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user.get("user_id"),
|
||||
user_type=current_user.get("type", "user"),
|
||||
is_service=current_user.get("type") == "service",
|
||||
role=current_user.get("role"),
|
||||
service_name=current_user.get("service", "none")
|
||||
)
|
||||
|
||||
# Skip access check for service-to-service calls
|
||||
is_service_call = current_user.get("type") == "service"
|
||||
if not is_service_call:
|
||||
# Verify user has access to the parent tenant
|
||||
access_info = await tenant_service.verify_user_access(current_user["user_id"], str(tenant_id))
|
||||
if not access_info.has_access:
|
||||
logger.warning(
|
||||
"Access denied to parent tenant",
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user.get("user_id")
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to parent tenant"
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
"Service-to-service call - bypassing access check",
|
||||
service=current_user.get("service"),
|
||||
tenant_id=str(tenant_id)
|
||||
)
|
||||
|
||||
# Get child tenants from repository
|
||||
from app.models.tenants import Tenant
|
||||
async with tenant_service.database_manager.get_session() as session:
|
||||
tenant_repo = TenantRepository(Tenant, session)
|
||||
child_tenants = await tenant_repo.get_child_tenants(str(tenant_id))
|
||||
|
||||
logger.debug(
|
||||
"Get tenant children successful",
|
||||
tenant_id=str(tenant_id),
|
||||
child_count=len(child_tenants)
|
||||
)
|
||||
|
||||
# Convert to plain dicts while still in session to avoid lazy-load issues
|
||||
child_dicts = []
|
||||
for child in child_tenants:
|
||||
# Handle subscription_tier safely - avoid lazy load
|
||||
try:
|
||||
# Try to get subscription_tier if subscriptions are already loaded
|
||||
sub_tier = child.__dict__.get('_subscription_tier_cache', 'enterprise')
|
||||
except:
|
||||
sub_tier = 'enterprise' # Default for enterprise children
|
||||
|
||||
child_dict = {
|
||||
'id': str(child.id),
|
||||
'name': child.name,
|
||||
'subdomain': child.subdomain,
|
||||
'business_type': child.business_type,
|
||||
'business_model': child.business_model,
|
||||
'address': child.address,
|
||||
'city': child.city,
|
||||
'postal_code': child.postal_code,
|
||||
'latitude': child.latitude,
|
||||
'longitude': child.longitude,
|
||||
'phone': child.phone,
|
||||
'email': child.email,
|
||||
'timezone': child.timezone,
|
||||
'owner_id': str(child.owner_id),
|
||||
'parent_tenant_id': str(child.parent_tenant_id) if child.parent_tenant_id else None,
|
||||
'tenant_type': child.tenant_type,
|
||||
'hierarchy_path': child.hierarchy_path,
|
||||
'subscription_tier': sub_tier, # Use the safely retrieved value
|
||||
'ml_model_trained': child.ml_model_trained,
|
||||
'last_training_date': child.last_training_date,
|
||||
'is_active': child.is_active,
|
||||
'is_demo': child.is_demo,
|
||||
'demo_session_id': child.demo_session_id,
|
||||
'created_at': child.created_at,
|
||||
'updated_at': child.updated_at
|
||||
}
|
||||
child_dicts.append(child_dict)
|
||||
|
||||
# Convert to Pydantic models outside the session without from_attributes
|
||||
child_responses = [TenantResponse(**child_dict) for child_dict in child_dicts]
|
||||
return child_responses
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Get tenant children failed",
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user.get("user_id"),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Get tenant children failed"
|
||||
)
|
||||
|
||||
|
||||
@router.get(route_builder.build_base_route("{tenant_id}/children/count", include_tenant_prefix=False))
|
||||
@track_endpoint_metrics("tenant_children_count")
|
||||
async def get_tenant_children_count(
|
||||
tenant_id: UUID = Path(..., description="Parent Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""
|
||||
Get count of child tenants for a parent tenant.
|
||||
This endpoint returns the number of active child tenants associated with the specified parent tenant.
|
||||
"""
|
||||
try:
|
||||
logger.info(
|
||||
"Get tenant children count request received",
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user.get("user_id")
|
||||
)
|
||||
|
||||
# Skip access check for service-to-service calls
|
||||
is_service_call = current_user.get("type") == "service"
|
||||
if not is_service_call:
|
||||
# Verify user has access to the parent tenant
|
||||
access_info = await tenant_service.verify_user_access(current_user["user_id"], str(tenant_id))
|
||||
if not access_info.has_access:
|
||||
logger.warning(
|
||||
"Access denied to parent tenant",
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user.get("user_id")
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to parent tenant"
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
"Service-to-service call - bypassing access check",
|
||||
service=current_user.get("service"),
|
||||
tenant_id=str(tenant_id)
|
||||
)
|
||||
|
||||
# Get child count from repository
|
||||
from app.models.tenants import Tenant
|
||||
async with tenant_service.database_manager.get_session() as session:
|
||||
tenant_repo = TenantRepository(Tenant, session)
|
||||
child_count = await tenant_repo.get_child_tenant_count(str(tenant_id))
|
||||
|
||||
logger.debug(
|
||||
"Get tenant children count successful",
|
||||
tenant_id=str(tenant_id),
|
||||
child_count=child_count
|
||||
)
|
||||
|
||||
return {
|
||||
"parent_tenant_id": str(tenant_id),
|
||||
"child_count": child_count
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Get tenant children count failed",
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user.get("user_id"),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Get tenant children count failed"
|
||||
)
|
||||
|
||||
|
||||
# Register the router in the main app
|
||||
def register_hierarchy_routes(app):
|
||||
"""Register hierarchy routes with the main application"""
|
||||
from shared.routing.route_builder import RouteBuilder
|
||||
route_builder = RouteBuilder("tenants")
|
||||
|
||||
# Include the hierarchy routes with proper tenant prefix
|
||||
app.include_router(
|
||||
router,
|
||||
prefix="/api/v1",
|
||||
tags=["tenant-hierarchy"]
|
||||
)
|
||||
@@ -140,6 +140,49 @@ async def update_tenant(
|
||||
detail="Tenant update failed"
|
||||
)
|
||||
|
||||
@router.get(route_builder.build_base_route("user/{user_id}/tenants", include_tenant_prefix=False), response_model=List[TenantResponse])
|
||||
@track_endpoint_metrics("user_tenants_list")
|
||||
async def get_user_tenants(
|
||||
user_id: str = Path(..., description="User ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Get all tenants accessible by a user"""
|
||||
|
||||
logger.info(
|
||||
"Get user tenants request received",
|
||||
user_id=user_id,
|
||||
requesting_user=current_user.get("user_id")
|
||||
)
|
||||
|
||||
if current_user.get("user_id") != user_id and current_user.get("type") != "service":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Can only access own tenants"
|
||||
)
|
||||
|
||||
try:
|
||||
tenants = await tenant_service.get_user_tenants(user_id)
|
||||
|
||||
logger.debug(
|
||||
"Get user tenants successful",
|
||||
user_id=user_id,
|
||||
tenant_count=len(tenants)
|
||||
)
|
||||
|
||||
return tenants
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Get user tenants failed",
|
||||
user_id=user_id,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get user tenants"
|
||||
)
|
||||
|
||||
@router.delete(route_builder.build_base_route("{tenant_id}", include_tenant_prefix=False))
|
||||
@track_endpoint_metrics("tenant_delete")
|
||||
async def delete_tenant(
|
||||
|
||||
@@ -90,4 +90,14 @@ class TenantSettings(BaseServiceSettings):
|
||||
STRIPE_SECRET_KEY: str = os.getenv("STRIPE_SECRET_KEY", "")
|
||||
STRIPE_WEBHOOK_SECRET: str = os.getenv("STRIPE_WEBHOOK_SECRET", "")
|
||||
|
||||
# ============================================================
|
||||
# SCHEDULER CONFIGURATION
|
||||
# ============================================================
|
||||
|
||||
# Usage tracking scheduler
|
||||
USAGE_TRACKING_ENABLED: bool = os.getenv("USAGE_TRACKING_ENABLED", "true").lower() == "true"
|
||||
USAGE_TRACKING_HOUR: int = int(os.getenv("USAGE_TRACKING_HOUR", "2"))
|
||||
USAGE_TRACKING_MINUTE: int = int(os.getenv("USAGE_TRACKING_MINUTE", "0"))
|
||||
USAGE_TRACKING_TIMEZONE: str = os.getenv("USAGE_TRACKING_TIMEZONE", "UTC")
|
||||
|
||||
settings = TenantSettings()
|
||||
|
||||
247
services/tenant/app/jobs/usage_tracking_scheduler.py
Normal file
247
services/tenant/app/jobs/usage_tracking_scheduler.py
Normal file
@@ -0,0 +1,247 @@
|
||||
"""
|
||||
Usage Tracking Scheduler
|
||||
Tracks daily usage snapshots for all active tenants
|
||||
"""
|
||||
import asyncio
|
||||
import structlog
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
from sqlalchemy import select, func
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class UsageTrackingScheduler:
|
||||
"""Scheduler for daily usage tracking"""
|
||||
|
||||
def __init__(self, db_manager, redis_client, config):
|
||||
self.db_manager = db_manager
|
||||
self.redis = redis_client
|
||||
self.config = config
|
||||
self._running = False
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
|
||||
def seconds_until_target_time(self) -> float:
|
||||
"""Calculate seconds until next target time (default 2am UTC)"""
|
||||
now = datetime.now(timezone.utc)
|
||||
target = now.replace(
|
||||
hour=self.config.USAGE_TRACKING_HOUR,
|
||||
minute=self.config.USAGE_TRACKING_MINUTE,
|
||||
second=0,
|
||||
microsecond=0
|
||||
)
|
||||
|
||||
if target <= now:
|
||||
target += timedelta(days=1)
|
||||
|
||||
return (target - now).total_seconds()
|
||||
|
||||
async def _get_tenant_usage(self, session, tenant_id: str) -> dict:
|
||||
"""Get current usage counts for a tenant"""
|
||||
usage = {}
|
||||
|
||||
try:
|
||||
# Import models here to avoid circular imports
|
||||
from app.models.tenants import TenantMember
|
||||
|
||||
# Users count
|
||||
result = await session.execute(
|
||||
select(func.count()).select_from(TenantMember).where(TenantMember.tenant_id == tenant_id)
|
||||
)
|
||||
usage['users'] = result.scalar() or 0
|
||||
|
||||
# Get counts from other services via their databases
|
||||
# For now, we'll track basic metrics. More metrics can be added by querying other service databases
|
||||
|
||||
# Training jobs today (from Redis quota tracking)
|
||||
today_key = f"quota:training_jobs:{tenant_id}:{datetime.now(timezone.utc).strftime('%Y-%m-%d')}"
|
||||
training_count = await self.redis.get(today_key)
|
||||
usage['training_jobs'] = int(training_count) if training_count else 0
|
||||
|
||||
# Forecasts today (from Redis quota tracking)
|
||||
forecast_key = f"quota:forecasts:{tenant_id}:{datetime.now(timezone.utc).strftime('%Y-%m-%d')}"
|
||||
forecast_count = await self.redis.get(forecast_key)
|
||||
usage['forecasts'] = int(forecast_count) if forecast_count else 0
|
||||
|
||||
# API calls this hour (from Redis quota tracking)
|
||||
hour_key = f"quota:api_calls:{tenant_id}:{datetime.now(timezone.utc).strftime('%Y-%m-%d-%H')}"
|
||||
api_count = await self.redis.get(hour_key)
|
||||
usage['api_calls'] = int(api_count) if api_count else 0
|
||||
|
||||
# Storage (placeholder - implement based on file storage system)
|
||||
usage['storage'] = 0.0
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting usage for tenant", tenant_id=tenant_id, error=str(e), exc_info=True)
|
||||
return {}
|
||||
|
||||
return usage
|
||||
|
||||
async def _track_metrics(self, tenant_id: str, usage: dict):
|
||||
"""Track metrics to Redis"""
|
||||
from app.api.usage_forecast import track_usage_snapshot
|
||||
|
||||
for metric_name, value in usage.items():
|
||||
try:
|
||||
await track_usage_snapshot(tenant_id, metric_name, value)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to track metric",
|
||||
tenant_id=tenant_id,
|
||||
metric=metric_name,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
async def _run_cycle(self):
|
||||
"""Execute one tracking cycle"""
|
||||
start_time = datetime.now(timezone.utc)
|
||||
logger.info("Starting daily usage tracking cycle")
|
||||
|
||||
try:
|
||||
async with self.db_manager.get_session() as session:
|
||||
# Import models here to avoid circular imports
|
||||
from app.models.tenants import Tenant, Subscription
|
||||
from sqlalchemy import select
|
||||
|
||||
# Get all active tenants
|
||||
result = await session.execute(
|
||||
select(Tenant, Subscription)
|
||||
.join(Subscription, Tenant.id == Subscription.tenant_id)
|
||||
.where(Tenant.is_active == True)
|
||||
.where(Subscription.status.in_(['active', 'trialing', 'cancelled']))
|
||||
)
|
||||
|
||||
tenants_data = result.all()
|
||||
total_tenants = len(tenants_data)
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
|
||||
logger.info(f"Found {total_tenants} active tenants to track")
|
||||
|
||||
# Process each tenant
|
||||
for tenant, subscription in tenants_data:
|
||||
try:
|
||||
usage = await self._get_tenant_usage(session, tenant.id)
|
||||
|
||||
if usage:
|
||||
await self._track_metrics(tenant.id, usage)
|
||||
success_count += 1
|
||||
else:
|
||||
logger.warning(
|
||||
"No usage data available for tenant",
|
||||
tenant_id=tenant.id
|
||||
)
|
||||
error_count += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error tracking tenant usage",
|
||||
tenant_id=tenant.id,
|
||||
error=str(e),
|
||||
exc_info=True
|
||||
)
|
||||
error_count += 1
|
||||
|
||||
end_time = datetime.now(timezone.utc)
|
||||
duration = (end_time - start_time).total_seconds()
|
||||
|
||||
logger.info(
|
||||
"Daily usage tracking completed",
|
||||
total_tenants=total_tenants,
|
||||
success=success_count,
|
||||
errors=error_count,
|
||||
duration_seconds=duration
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Usage tracking cycle failed", error=str(e), exc_info=True)
|
||||
|
||||
async def _run_scheduler(self):
|
||||
"""Main scheduler loop"""
|
||||
logger.info(
|
||||
"Usage tracking scheduler loop started",
|
||||
target_hour=self.config.USAGE_TRACKING_HOUR,
|
||||
target_minute=self.config.USAGE_TRACKING_MINUTE
|
||||
)
|
||||
|
||||
# Initial delay to target time
|
||||
delay = self.seconds_until_target_time()
|
||||
logger.info(f"Waiting {delay/3600:.2f} hours until next run at {self.config.USAGE_TRACKING_HOUR:02d}:{self.config.USAGE_TRACKING_MINUTE:02d} UTC")
|
||||
|
||||
try:
|
||||
await asyncio.sleep(delay)
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Scheduler cancelled during initial delay")
|
||||
return
|
||||
|
||||
while self._running:
|
||||
try:
|
||||
await self._run_cycle()
|
||||
except Exception as e:
|
||||
logger.error("Scheduler cycle error", error=str(e), exc_info=True)
|
||||
|
||||
# Wait 24 hours until next run
|
||||
try:
|
||||
await asyncio.sleep(86400)
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Scheduler cancelled during sleep")
|
||||
break
|
||||
|
||||
def start(self):
|
||||
"""Start the scheduler"""
|
||||
if not self.config.USAGE_TRACKING_ENABLED:
|
||||
logger.info("Usage tracking scheduler disabled by configuration")
|
||||
return
|
||||
|
||||
if self._running:
|
||||
logger.warning("Usage tracking scheduler already running")
|
||||
return
|
||||
|
||||
self._running = True
|
||||
self._task = asyncio.create_task(self._run_scheduler())
|
||||
logger.info("Usage tracking scheduler started successfully")
|
||||
|
||||
async def stop(self):
|
||||
"""Stop the scheduler gracefully"""
|
||||
if not self._running:
|
||||
logger.debug("Scheduler not running, nothing to stop")
|
||||
return
|
||||
|
||||
logger.info("Stopping usage tracking scheduler")
|
||||
self._running = False
|
||||
|
||||
if self._task:
|
||||
self._task.cancel()
|
||||
try:
|
||||
await self._task
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Scheduler task cancelled successfully")
|
||||
|
||||
logger.info("Usage tracking scheduler stopped")
|
||||
|
||||
|
||||
# Global instance
|
||||
_scheduler: Optional[UsageTrackingScheduler] = None
|
||||
|
||||
|
||||
async def start_scheduler(db_manager, redis_client, config):
|
||||
"""Start the usage tracking scheduler"""
|
||||
global _scheduler
|
||||
|
||||
try:
|
||||
_scheduler = UsageTrackingScheduler(db_manager, redis_client, config)
|
||||
_scheduler.start()
|
||||
logger.info("Usage tracking scheduler module initialized")
|
||||
except Exception as e:
|
||||
logger.error("Failed to start usage tracking scheduler", error=str(e), exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
async def stop_scheduler():
|
||||
"""Stop the usage tracking scheduler"""
|
||||
global _scheduler
|
||||
|
||||
if _scheduler:
|
||||
await _scheduler.stop()
|
||||
_scheduler = None
|
||||
logger.info("Usage tracking scheduler module stopped")
|
||||
@@ -7,7 +7,7 @@ from fastapi import FastAPI
|
||||
from sqlalchemy import text
|
||||
from app.core.config import settings
|
||||
from app.core.database import database_manager
|
||||
from app.api import tenants, tenant_members, tenant_operations, webhooks, internal_demo, plans, subscription, tenant_settings, whatsapp_admin, usage_forecast, enterprise_upgrade, tenant_locations
|
||||
from app.api import tenants, tenant_members, tenant_operations, webhooks, internal_demo, plans, subscription, tenant_settings, whatsapp_admin, usage_forecast, enterprise_upgrade, tenant_locations, tenant_hierarchy
|
||||
from shared.service_base import StandardFastAPIService
|
||||
|
||||
|
||||
@@ -71,10 +71,30 @@ class TenantService(StandardFastAPIService):
|
||||
from app.models.tenant_settings import TenantSettings
|
||||
self.logger.info("Tenant models imported successfully")
|
||||
|
||||
# Initialize Redis
|
||||
from shared.redis_utils import initialize_redis, get_redis_client
|
||||
await initialize_redis(settings.REDIS_URL, db=settings.REDIS_DB, max_connections=20)
|
||||
redis_client = await get_redis_client()
|
||||
self.logger.info("Redis initialized successfully")
|
||||
|
||||
# Start usage tracking scheduler
|
||||
from app.jobs.usage_tracking_scheduler import start_scheduler
|
||||
await start_scheduler(self.database_manager, redis_client, settings)
|
||||
self.logger.info("Usage tracking scheduler started")
|
||||
|
||||
async def on_shutdown(self, app: FastAPI):
|
||||
"""Custom shutdown logic for tenant service"""
|
||||
# Stop usage tracking scheduler
|
||||
from app.jobs.usage_tracking_scheduler import stop_scheduler
|
||||
await stop_scheduler()
|
||||
self.logger.info("Usage tracking scheduler stopped")
|
||||
|
||||
# Close Redis connection
|
||||
from shared.redis_utils import close_redis
|
||||
await close_redis()
|
||||
self.logger.info("Redis connection closed")
|
||||
|
||||
# Database cleanup is handled by the base class
|
||||
pass
|
||||
|
||||
def get_service_features(self):
|
||||
"""Return tenant-specific features"""
|
||||
@@ -124,6 +144,7 @@ service.add_router(tenant_operations.router, tags=["tenant-operations"])
|
||||
service.add_router(webhooks.router, tags=["webhooks"])
|
||||
service.add_router(enterprise_upgrade.router, tags=["enterprise"]) # Enterprise tier upgrade endpoints
|
||||
service.add_router(tenant_locations.router, tags=["tenant-locations"]) # Tenant locations endpoints
|
||||
service.add_router(tenant_hierarchy.router, tags=["tenant-hierarchy"]) # Tenant hierarchy endpoints
|
||||
service.add_router(internal_demo.router, tags=["internal"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -4,11 +4,8 @@ Business logic services for tenant operations
|
||||
"""
|
||||
|
||||
from .tenant_service import TenantService, EnhancedTenantService
|
||||
from .messaging import publish_tenant_created, publish_member_added
|
||||
|
||||
__all__ = [
|
||||
"TenantService",
|
||||
"EnhancedTenantService",
|
||||
"publish_tenant_created",
|
||||
"publish_member_added"
|
||||
"EnhancedTenantService"
|
||||
]
|
||||
@@ -1,74 +0,0 @@
|
||||
# services/tenant/app/services/messaging.py
|
||||
"""
|
||||
Tenant service messaging for event publishing
|
||||
"""
|
||||
from shared.messaging.rabbitmq import RabbitMQClient
|
||||
from app.core.config import settings
|
||||
import structlog
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# Single global instance
|
||||
data_publisher = RabbitMQClient(settings.RABBITMQ_URL, "data-service")
|
||||
|
||||
async def publish_tenant_created(tenant_id: str, owner_id: str, tenant_name: str):
|
||||
"""Publish tenant created event"""
|
||||
try:
|
||||
await data_publisher.publish_event(
|
||||
"tenant.created",
|
||||
{
|
||||
"tenant_id": tenant_id,
|
||||
"owner_id": owner_id,
|
||||
"tenant_name": tenant_name,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to publish tenant.created event: {e}")
|
||||
|
||||
async def publish_member_added(tenant_id: str, user_id: str, role: str):
|
||||
"""Publish member added event"""
|
||||
try:
|
||||
await data_publisher.publish_event(
|
||||
"tenant.member.added",
|
||||
{
|
||||
"tenant_id": tenant_id,
|
||||
"user_id": user_id,
|
||||
"role": role,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to publish tenant.member.added event: {e}")
|
||||
|
||||
async def publish_tenant_deleted_event(tenant_id: str, deletion_stats: Dict[str, Any]):
|
||||
"""Publish tenant deletion event to message queue"""
|
||||
try:
|
||||
await data_publisher.publish_event(
|
||||
exchange="tenant_events",
|
||||
routing_key="tenant.deleted",
|
||||
message={
|
||||
"event_type": "tenant_deleted",
|
||||
"tenant_id": tenant_id,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"deletion_stats": deletion_stats
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Failed to publish tenant deletion event", error=str(e))
|
||||
|
||||
async def publish_tenant_deleted(tenant_id: str, tenant_name: str):
|
||||
"""Publish tenant deleted event (simple version)"""
|
||||
try:
|
||||
await data_publisher.publish_event(
|
||||
"tenant.deleted",
|
||||
{
|
||||
"tenant_id": tenant_id,
|
||||
"tenant_name": tenant_name,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to publish tenant.deleted event: {e}")
|
||||
@@ -15,7 +15,6 @@ from app.schemas.tenants import (
|
||||
BakeryRegistration, TenantResponse, TenantAccessResponse,
|
||||
TenantUpdate, TenantMemberResponse
|
||||
)
|
||||
from app.services.messaging import publish_tenant_created, publish_member_added
|
||||
from shared.database.exceptions import DatabaseError, ValidationError, DuplicateRecordError
|
||||
from shared.database.base import create_database_manager
|
||||
from shared.database.unit_of_work import UnitOfWork
|
||||
@@ -27,8 +26,9 @@ logger = structlog.get_logger()
|
||||
class EnhancedTenantService:
|
||||
"""Enhanced tenant management business logic using repository pattern with dependency injection"""
|
||||
|
||||
def __init__(self, database_manager=None):
|
||||
def __init__(self, database_manager=None, event_publisher=None):
|
||||
self.database_manager = database_manager or create_database_manager()
|
||||
self.event_publisher = event_publisher
|
||||
|
||||
async def _init_repositories(self, session):
|
||||
"""Initialize repositories with session"""
|
||||
@@ -165,11 +165,21 @@ class EnhancedTenantService:
|
||||
# Commit the transaction
|
||||
await uow.commit()
|
||||
|
||||
# Publish event
|
||||
try:
|
||||
await publish_tenant_created(str(tenant.id), owner_id, bakery_data.name)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to publish tenant created event", error=str(e))
|
||||
# Publish tenant created event
|
||||
if self.event_publisher:
|
||||
try:
|
||||
await self.event_publisher.publish_business_event(
|
||||
event_type="tenant.created",
|
||||
tenant_id=str(tenant.id),
|
||||
data={
|
||||
"tenant_id": str(tenant.id),
|
||||
"owner_id": owner_id,
|
||||
"name": bakery_data.name,
|
||||
"created_at": datetime.now(timezone.utc).isoformat()
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to publish tenant created event", error=str(e))
|
||||
|
||||
# Automatically create location-context with city information
|
||||
# This is non-blocking - failure won't prevent tenant creation
|
||||
@@ -557,11 +567,22 @@ class EnhancedTenantService:
|
||||
|
||||
member = await self.member_repo.create_membership(membership_data)
|
||||
|
||||
# Publish event
|
||||
try:
|
||||
await publish_member_added(tenant_id, user_id, role)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to publish member added event", error=str(e))
|
||||
# Publish member added event
|
||||
if self.event_publisher:
|
||||
try:
|
||||
await self.event_publisher.publish_business_event(
|
||||
event_type="tenant.member.added",
|
||||
tenant_id=tenant_id,
|
||||
data={
|
||||
"tenant_id": tenant_id,
|
||||
"user_id": user_id,
|
||||
"role": role,
|
||||
"invited_by": invited_by,
|
||||
"added_at": datetime.now(timezone.utc).isoformat()
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to publish member added event", error=str(e))
|
||||
|
||||
logger.info("Team member added successfully",
|
||||
tenant_id=tenant_id,
|
||||
@@ -1015,10 +1036,29 @@ class EnhancedTenantService:
|
||||
detail=f"Failed to delete tenant: {str(e)}"
|
||||
)
|
||||
|
||||
# Publish deletion event for other services
|
||||
# Publish deletion event for other services using unified messaging
|
||||
try:
|
||||
from app.services.messaging import publish_tenant_deleted
|
||||
await publish_tenant_deleted(tenant_id, tenant.name)
|
||||
from shared.messaging import initialize_service_publisher, EVENT_TYPES
|
||||
from app.core.config import settings
|
||||
|
||||
# Create a temporary publisher to send the event using the unified helper
|
||||
temp_rabbitmq_client, temp_publisher = await initialize_service_publisher("tenant-service", settings.RABBITMQ_URL)
|
||||
if temp_publisher:
|
||||
try:
|
||||
await temp_publisher.publish_business_event(
|
||||
event_type=EVENT_TYPES.TENANT.TENANT_DELETED,
|
||||
tenant_id=tenant_id,
|
||||
data={
|
||||
"tenant_id": tenant_id,
|
||||
"tenant_name": tenant.name,
|
||||
"deleted_at": datetime.now(timezone.utc).isoformat()
|
||||
}
|
||||
)
|
||||
finally:
|
||||
if temp_rabbitmq_client:
|
||||
await temp_rabbitmq_client.disconnect()
|
||||
else:
|
||||
logger.warning("Could not connect to RabbitMQ to publish tenant deletion event")
|
||||
except Exception as e:
|
||||
logger.warning("Failed to publish tenant deletion event",
|
||||
tenant_id=tenant_id,
|
||||
|
||||
Reference in New Issue
Block a user