# services/tenant/app/services/tenant_service.py """ Tenant service business logic """ import structlog from datetime import datetime, timezone from typing import Optional, List, Dict, Any from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, update, and_ from fastapi import HTTPException, status import uuid import json from app.models.tenants import Tenant, TenantMember from app.schemas.tenants import BakeryRegistration, TenantResponse, TenantAccessResponse, TenantUpdate from app.services.messaging import publish_tenant_created, publish_member_added logger = structlog.get_logger() class TenantService: """Tenant management business logic""" @staticmethod async def create_bakery(bakery_data: BakeryRegistration, owner_id: str, db: AsyncSession) -> TenantResponse: """Create a new bakery/tenant""" try: # Generate subdomain if not provided subdomain = bakery_data.name.lower().replace(' ', '-').replace('á', 'a').replace('é', 'e').replace('í', 'i').replace('ó', 'o').replace('ú', 'u') subdomain = ''.join(c for c in subdomain if c.isalnum() or c == '-') # Check if subdomain already exists result = await db.execute( select(Tenant).where(Tenant.subdomain == subdomain) ) if result.scalar_one_or_none(): subdomain = f"{subdomain}-{uuid.uuid4().hex[:6]}" # Create tenant tenant = Tenant( name=bakery_data.name, subdomain=subdomain, business_type=bakery_data.business_type, address=bakery_data.address, city=bakery_data.city, postal_code=bakery_data.postal_code, phone=bakery_data.phone, owner_id=owner_id, is_active=True ) db.add(tenant) await db.commit() await db.refresh(tenant) # Create owner membership owner_membership = TenantMember( tenant_id=tenant.id, user_id=owner_id, role="owner", permissions=json.dumps(["read", "write", "admin", "delete"]), is_active=True, joined_at=datetime.now(timezone.utc) ) db.add(owner_membership) await db.commit() # Publish event await publish_tenant_created(str(tenant.id), owner_id, bakery_data.name) logger.info(f"Bakery created: {bakery_data.name} (ID: {tenant.id})") return TenantResponse.from_orm(tenant) except Exception as e: await db.rollback() logger.error(f"Error creating bakery: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create bakery" ) @staticmethod async def verify_user_access(user_id: str, tenant_id: str, db: AsyncSession) -> TenantAccessResponse: """Verify if user has access to tenant""" try: # Check if user is tenant member result = await db.execute( select(TenantMember).where( and_( TenantMember.user_id == user_id, TenantMember.tenant_id == tenant_id, TenantMember.is_active == True ) ) ) membership = result.scalar_one_or_none() if not membership: return TenantAccessResponse( has_access=False, role="none", permissions=[] ) # Parse permissions permissions = [] if membership.permissions: try: permissions = json.loads(membership.permissions) except: permissions = [] return TenantAccessResponse( has_access=True, role=membership.role, permissions=permissions ) except Exception as e: logger.error(f"Error verifying user access: {e}") return TenantAccessResponse( has_access=False, role="none", permissions=[] ) @staticmethod async def get_user_tenants(user_id: str, db: AsyncSession) -> List[TenantResponse]: """Get all tenants accessible by user""" try: # Get user's tenant memberships result = await db.execute( select(Tenant) .join(TenantMember, Tenant.id == TenantMember.tenant_id) .where( and_( TenantMember.user_id == user_id, TenantMember.is_active == True, Tenant.is_active == True ) ) .order_by(Tenant.name) ) tenants = result.scalars().all() return [TenantResponse.from_orm(tenant) for tenant in tenants] except Exception as e: logger.error(f"Error getting user tenants: {e}") return [] @staticmethod async def get_tenant_by_id(tenant_id: str, db: AsyncSession) -> Optional[TenantResponse]: """Get tenant by ID""" try: result = await db.execute( select(Tenant).where(Tenant.id == tenant_id) ) tenant = result.scalar_one_or_none() if tenant: return TenantResponse.from_orm(tenant) return None except Exception as e: logger.error(f"Error getting tenant: {e}") return None @staticmethod async def update_tenant(tenant_id: str, update_data: TenantUpdate, user_id: str, db: AsyncSession) -> TenantResponse: """Update tenant information""" try: # Verify user has admin access access = await TenantService.verify_user_access(user_id, tenant_id, db) if not access.has_access or access.role not in ["owner", "admin"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions to update tenant" ) # Update tenant update_values = update_data.dict(exclude_unset=True) if update_values: update_values["updated_at"] = datetime.now(timezone.utc) await db.execute( update(Tenant) .where(Tenant.id == tenant_id) .values(**update_values) ) await db.commit() # Get updated tenant result = await db.execute( select(Tenant).where(Tenant.id == tenant_id) ) tenant = result.scalar_one() logger.info(f"Tenant updated: {tenant.name} (ID: {tenant_id})") return TenantResponse.from_orm(tenant) except HTTPException: raise except Exception as e: await db.rollback() logger.error(f"Error updating tenant: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update tenant" ) @staticmethod async def add_team_member(tenant_id: str, user_id: str, role: str, invited_by: str, db: AsyncSession) -> TenantMemberResponse: """Add a team member to tenant""" try: # Verify inviter has admin access access = await TenantService.verify_user_access(invited_by, tenant_id, db) if not access.has_access or access.role not in ["owner", "admin"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions to add team members" ) # Check if user is already a member result = await db.execute( select(TenantMember).where( and_( TenantMember.tenant_id == tenant_id, TenantMember.user_id == user_id ) ) ) existing_member = result.scalar_one_or_none() if existing_member: if existing_member.is_active: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="User is already a member of this tenant" ) else: # Reactivate existing membership existing_member.is_active = True existing_member.role = role existing_member.joined_at = datetime.now(timezone.utc) await db.commit() return TenantMemberResponse.from_orm(existing_member) # Create new membership permissions = ["read"] if role in ["admin", "owner"]: permissions.extend(["write", "admin"]) if role == "owner": permissions.append("delete") member = TenantMember( tenant_id=tenant_id, user_id=user_id, role=role, permissions=json.dumps(permissions), invited_by=invited_by, is_active=True, joined_at=datetime.now(timezone.utc) ) db.add(member) await db.commit() await db.refresh(member) # Publish event await publish_member_added(tenant_id, user_id, role) logger.info(f"Team member added: {user_id} to tenant {tenant_id} as {role}") return TenantMemberResponse.from_orm(member) except HTTPException: raise except Exception as e: await db.rollback() logger.error(f"Error adding team member: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to add team member" )