# app/services/company_service.py """ Company service for managing company operations. This service handles CRUD operations for companies and company-vendor relationships. """ import logging import secrets import string from sqlalchemy import func, select from sqlalchemy.orm import Session, joinedload from app.exceptions import CompanyNotFoundException, UserNotFoundException from models.database.company import Company from models.database.user import User from models.schema.company import CompanyCreate, CompanyTransferOwnership, CompanyUpdate logger = logging.getLogger(__name__) class CompanyService: """Service for managing companies.""" def __init__(self): """Initialize company service.""" def create_company_with_owner( self, db: Session, company_data: CompanyCreate ) -> tuple[Company, User, str]: """ Create a new company with an owner user account. Args: db: Database session company_data: Company creation data Returns: Tuple of (company, owner_user, temporary_password) """ # Import AuthManager for password hashing (same pattern as admin_service) from middleware.auth import AuthManager auth_manager = AuthManager() # Check if owner email already exists existing_user = db.execute( select(User).where(User.email == company_data.owner_email) ).scalar_one_or_none() if existing_user: # Use existing user as owner owner_user = existing_user temp_password = None logger.info(f"Using existing user {owner_user.email} as company owner") else: # Generate temporary password for owner temp_password = self._generate_temp_password() # Create new owner user owner_user = User( username=company_data.owner_email.split("@")[0], email=company_data.owner_email, hashed_password=auth_manager.hash_password(temp_password), role="user", is_active=True, is_email_verified=True, ) db.add(owner_user) db.flush() # Get owner_user.id logger.info(f"Created new owner user: {owner_user.email}") # Create company company = Company( name=company_data.name, description=company_data.description, owner_user_id=owner_user.id, contact_email=company_data.contact_email, contact_phone=company_data.contact_phone, website=company_data.website, business_address=company_data.business_address, tax_number=company_data.tax_number, is_active=True, is_verified=False, ) db.add(company) db.flush() logger.info(f"Created company: {company.name} (ID: {company.id})") return company, owner_user, temp_password def get_company_by_id(self, db: Session, company_id: int) -> Company: """ Get company by ID. Args: db: Database session company_id: Company ID Returns: Company object Raises: CompanyNotFoundException: If company not found """ company = ( db.execute( select(Company) .where(Company.id == company_id) .options(joinedload(Company.vendors)) ) .unique() .scalar_one_or_none() ) if not company: raise CompanyNotFoundException(company_id) return company def get_companies( self, db: Session, skip: int = 0, limit: int = 100, search: str | None = None, is_active: bool | None = None, is_verified: bool | None = None, ) -> tuple[list[Company], int]: """ Get paginated list of companies with optional filters. Args: db: Database session skip: Number of records to skip limit: Maximum number of records to return search: Search term for company name is_active: Filter by active status is_verified: Filter by verified status Returns: Tuple of (companies list, total count) """ query = select(Company).options(joinedload(Company.vendors)) # Apply filters if search: query = query.where(Company.name.ilike(f"%{search}%")) if is_active is not None: query = query.where(Company.is_active == is_active) if is_verified is not None: query = query.where(Company.is_verified == is_verified) # Get total count count_query = select(func.count()).select_from(query.subquery()) total = db.execute(count_query).scalar() # Apply pagination and order query = query.order_by(Company.name).offset(skip).limit(limit) # Use unique() when using joinedload with collections to avoid duplicate rows companies = list(db.execute(query).scalars().unique().all()) return companies, total def update_company( self, db: Session, company_id: int, company_data: CompanyUpdate ) -> Company: """ Update company information. Args: db: Database session company_id: Company ID company_data: Updated company data Returns: Updated company Raises: CompanyNotFoundException: If company not found """ company = self.get_company_by_id(db, company_id) # Update only provided fields update_data = company_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(company, field, value) db.flush() logger.info(f"Updated company ID {company_id}") return company def delete_company(self, db: Session, company_id: int) -> None: """ Delete a company and all associated vendors. Args: db: Database session company_id: Company ID Raises: CompanyNotFoundException: If company not found """ company = self.get_company_by_id(db, company_id) # Due to cascade="all, delete-orphan", associated vendors will be deleted db.delete(company) db.flush() logger.info(f"Deleted company ID {company_id} and associated vendors") def toggle_verification( self, db: Session, company_id: int, is_verified: bool ) -> Company: """ Toggle company verification status. Args: db: Database session company_id: Company ID is_verified: New verification status Returns: Updated company Raises: CompanyNotFoundException: If company not found """ company = self.get_company_by_id(db, company_id) company.is_verified = is_verified db.flush() logger.info(f"Company ID {company_id} verification set to {is_verified}") return company def toggle_active(self, db: Session, company_id: int, is_active: bool) -> Company: """ Toggle company active status. Args: db: Database session company_id: Company ID is_active: New active status Returns: Updated company Raises: CompanyNotFoundException: If company not found """ company = self.get_company_by_id(db, company_id) company.is_active = is_active db.flush() logger.info(f"Company ID {company_id} active status set to {is_active}") return company def transfer_ownership( self, db: Session, company_id: int, transfer_data: CompanyTransferOwnership, ) -> tuple[Company, User, User]: """ Transfer company ownership to another user. This is a critical operation that: - Changes the company's owner_user_id - All vendors under the company automatically inherit the new owner - Logs the transfer for audit purposes Args: db: Database session company_id: Company ID transfer_data: Transfer ownership data Returns: Tuple of (company, old_owner, new_owner) Raises: CompanyNotFoundException: If company not found UserNotFoundException: If new owner user not found ValueError: If trying to transfer to current owner """ # Get company company = self.get_company_by_id(db, company_id) old_owner_id = company.owner_user_id # Get old owner old_owner = db.execute( select(User).where(User.id == old_owner_id) ).scalar_one_or_none() if not old_owner: raise UserNotFoundException(str(old_owner_id)) # Get new owner new_owner = db.execute( select(User).where(User.id == transfer_data.new_owner_user_id) ).scalar_one_or_none() if not new_owner: raise UserNotFoundException(str(transfer_data.new_owner_user_id)) # Prevent transferring to same owner if old_owner_id == transfer_data.new_owner_user_id: raise ValueError("Cannot transfer ownership to the current owner") # Update company owner (vendors inherit ownership via company relationship) company.owner_user_id = new_owner.id db.flush() logger.info( f"Company {company.id} ({company.name}) ownership transferred " f"from user {old_owner.id} ({old_owner.email}) " f"to user {new_owner.id} ({new_owner.email}). " f"Reason: {transfer_data.transfer_reason or 'Not specified'}" ) return company, old_owner, new_owner def _generate_temp_password(self, length: int = 12) -> str: """Generate secure temporary password.""" alphabet = string.ascii_letters + string.digits + "!@#$%^&*" return "".join(secrets.choice(alphabet) for _ in range(length)) # Create service instance following the same pattern as other services company_service = CompanyService()