""" M365 Tenant service layer for business logic and database operations. This module handles all database operations for M365 tenants, providing a clean separation between the API routes and data access layer. """ from typing import Optional from uuid import UUID from fastapi import HTTPException, status from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from api.models.m365_tenant import M365Tenant from api.models.client import Client from api.schemas.m365_tenant import M365TenantCreate, M365TenantUpdate def get_m365_tenants(db: Session, skip: int = 0, limit: int = 100) -> tuple[list[M365Tenant], int]: """ Retrieve a paginated list of M365 tenants. Args: db: Database session skip: Number of records to skip (for pagination) limit: Maximum number of records to return Returns: tuple: (list of M365 tenants, total count) Example: ```python tenants, total = get_m365_tenants(db, skip=0, limit=50) print(f"Retrieved {len(tenants)} of {total} M365 tenants") ``` """ # Get total count total = db.query(M365Tenant).count() # Get paginated results, ordered by created_at descending (newest first) tenants = ( db.query(M365Tenant) .order_by(M365Tenant.created_at.desc()) .offset(skip) .limit(limit) .all() ) return tenants, total def get_m365_tenant_by_id(db: Session, tenant_id: UUID) -> M365Tenant: """ Retrieve a single M365 tenant by its ID. Args: db: Database session tenant_id: UUID of the M365 tenant to retrieve Returns: M365Tenant: The M365 tenant object Raises: HTTPException: 404 if M365 tenant not found Example: ```python tenant = get_m365_tenant_by_id(db, tenant_id) print(f"Found tenant: {tenant.tenant_name}") ``` """ tenant = db.query(M365Tenant).filter(M365Tenant.id == str(tenant_id)).first() if not tenant: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"M365 tenant with ID {tenant_id} not found" ) return tenant def get_m365_tenant_by_tenant_id(db: Session, tenant_id: str) -> Optional[M365Tenant]: """ Retrieve an M365 tenant by its Microsoft tenant ID. Args: db: Database session tenant_id: Microsoft tenant ID to search for Returns: Optional[M365Tenant]: The M365 tenant if found, None otherwise Example: ```python tenant = get_m365_tenant_by_tenant_id(db, "abc12345-6789-0def-1234-56789abcdef0") if tenant: print(f"Found tenant: {tenant.tenant_name}") ``` """ return db.query(M365Tenant).filter(M365Tenant.tenant_id == tenant_id).first() def get_m365_tenants_by_client(db: Session, client_id: UUID, skip: int = 0, limit: int = 100) -> tuple[list[M365Tenant], int]: """ Retrieve M365 tenants for a specific client. Args: db: Database session client_id: UUID of the client skip: Number of records to skip (for pagination) limit: Maximum number of records to return Returns: tuple: (list of M365 tenants, total count) Raises: HTTPException: 404 if client not found Example: ```python tenants, total = get_m365_tenants_by_client(db, client_id, skip=0, limit=50) print(f"Client has {total} M365 tenants") ``` """ # Verify client exists client = db.query(Client).filter(Client.id == str(client_id)).first() if not client: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Client with ID {client_id} not found" ) # Get total count for this client total = db.query(M365Tenant).filter(M365Tenant.client_id == str(client_id)).count() # Get paginated results tenants = ( db.query(M365Tenant) .filter(M365Tenant.client_id == str(client_id)) .order_by(M365Tenant.created_at.desc()) .offset(skip) .limit(limit) .all() ) return tenants, total def create_m365_tenant(db: Session, tenant_data: M365TenantCreate) -> M365Tenant: """ Create a new M365 tenant. Args: db: Database session tenant_data: M365 tenant creation data Returns: M365Tenant: The created M365 tenant object Raises: HTTPException: 404 if client_id provided and client doesn't exist HTTPException: 409 if M365 tenant with tenant_id already exists HTTPException: 500 if database error occurs Example: ```python tenant_data = M365TenantCreate( tenant_id="abc12345-6789-0def-1234-56789abcdef0", tenant_name="dataforth.com", client_id="123e4567-e89b-12d3-a456-426614174000" ) tenant = create_m365_tenant(db, tenant_data) print(f"Created M365 tenant: {tenant.id}") ``` """ # Validate client exists if client_id provided if tenant_data.client_id: client = db.query(Client).filter(Client.id == str(tenant_data.client_id)).first() if not client: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Client with ID {tenant_data.client_id} not found" ) # Check if M365 tenant with tenant_id already exists existing_tenant = get_m365_tenant_by_tenant_id(db, tenant_data.tenant_id) if existing_tenant: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"M365 tenant with tenant_id '{tenant_data.tenant_id}' already exists" ) try: # Create new M365 tenant instance db_tenant = M365Tenant(**tenant_data.model_dump()) # Add to database db.add(db_tenant) db.commit() db.refresh(db_tenant) return db_tenant except IntegrityError as e: db.rollback() # Handle unique constraint violations if "tenant_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"M365 tenant with tenant_id '{tenant_data.tenant_id}' already exists" ) elif "client_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Client with ID {tenant_data.client_id} not found" ) else: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Database error: {str(e)}" ) except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to create M365 tenant: {str(e)}" ) def update_m365_tenant(db: Session, tenant_id: UUID, tenant_data: M365TenantUpdate) -> M365Tenant: """ Update an existing M365 tenant. Args: db: Database session tenant_id: UUID of the M365 tenant to update tenant_data: M365 tenant update data (only provided fields will be updated) Returns: M365Tenant: The updated M365 tenant object Raises: HTTPException: 404 if M365 tenant not found or client_id provided and client doesn't exist HTTPException: 409 if update would violate unique constraints HTTPException: 500 if database error occurs Example: ```python update_data = M365TenantUpdate( admin_email="admin@example.com", notes="Updated tenant information" ) tenant = update_m365_tenant(db, tenant_id, update_data) print(f"Updated M365 tenant: {tenant.tenant_name}") ``` """ # Get existing M365 tenant tenant = get_m365_tenant_by_id(db, tenant_id) try: # Update only provided fields update_data = tenant_data.model_dump(exclude_unset=True) # If updating client_id, validate client exists if "client_id" in update_data and update_data["client_id"] is not None: client = db.query(Client).filter(Client.id == str(update_data["client_id"])).first() if not client: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Client with ID {update_data['client_id']} not found" ) # If updating tenant_id, check if new tenant_id is already taken if "tenant_id" in update_data and update_data["tenant_id"] != tenant.tenant_id: existing = get_m365_tenant_by_tenant_id(db, update_data["tenant_id"]) if existing: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"M365 tenant with tenant_id '{update_data['tenant_id']}' already exists" ) # Apply updates for field, value in update_data.items(): setattr(tenant, field, value) db.commit() db.refresh(tenant) return tenant except HTTPException: db.rollback() raise except IntegrityError as e: db.rollback() if "tenant_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="M365 tenant with this tenant_id already exists" ) elif "client_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Client not found" ) else: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Database error: {str(e)}" ) except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update M365 tenant: {str(e)}" ) def delete_m365_tenant(db: Session, tenant_id: UUID) -> dict: """ Delete an M365 tenant by its ID. Args: db: Database session tenant_id: UUID of the M365 tenant to delete Returns: dict: Success message Raises: HTTPException: 404 if M365 tenant not found HTTPException: 500 if database error occurs Example: ```python result = delete_m365_tenant(db, tenant_id) print(result["message"]) # "M365 tenant deleted successfully" ``` """ # Get existing M365 tenant (raises 404 if not found) tenant = get_m365_tenant_by_id(db, tenant_id) try: db.delete(tenant) db.commit() return { "message": "M365 tenant deleted successfully", "tenant_id": str(tenant_id) } except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to delete M365 tenant: {str(e)}" )