from typing import Annotated from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.core.database import get_db from app.core.audit import log_audit from app.models.user import User from app.models.plan_limits import PlanLimits from app.models.account import Account from app.models.account_limit_override import AccountLimitOverride from app.schemas.admin import ( PlanLimitResponse, PlanLimitUpdate, AccountOverrideCreate, AccountOverrideUpdate, AccountOverrideResponse, ) from app.api.deps import require_admin router = APIRouter(prefix="/admin", tags=["admin-plan-limits"]) @router.get("/plan-limits", response_model=list[PlanLimitResponse]) async def list_plan_limits( db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(require_admin)], ): """List all plan limit configurations.""" result = await db.execute(select(PlanLimits)) return result.scalars().all() @router.put("/plan-limits", response_model=PlanLimitResponse) async def update_plan_limits( data: PlanLimitUpdate, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(require_admin)], ): """Update a plan's limits.""" result = await db.execute(select(PlanLimits).where(PlanLimits.plan == data.plan)) plan = result.scalar_one_or_none() if not plan: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Plan not found") plan.max_trees = data.max_trees plan.max_sessions_per_month = data.max_sessions_per_month plan.max_users = data.max_users plan.custom_branding = data.custom_branding plan.priority_support = data.priority_support plan.export_formats = data.export_formats await log_audit(db, current_user.id, "plan_limits.update", "plan_limits", details={"plan": data.plan}) await db.commit() await db.refresh(plan) return plan @router.get("/account-overrides", response_model=list[AccountOverrideResponse]) async def list_account_overrides( db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(require_admin)], ): """List all account limit overrides.""" query = ( select( AccountLimitOverride, Account.name.label("account_name"), Account.display_code.label("account_display_code"), ) .outerjoin(Account, AccountLimitOverride.account_id == Account.id) .order_by(AccountLimitOverride.created_at.desc()) ) result = await db.execute(query) rows = result.all() return [ AccountOverrideResponse( id=row.AccountLimitOverride.id, account_id=row.AccountLimitOverride.account_id, account_name=row.account_name, account_display_code=row.account_display_code, override_max_trees=row.AccountLimitOverride.override_max_trees, override_max_sessions_per_month=row.AccountLimitOverride.override_max_sessions_per_month, override_max_users=row.AccountLimitOverride.override_max_users, note=row.AccountLimitOverride.note, created_at=row.AccountLimitOverride.created_at, updated_at=row.AccountLimitOverride.updated_at, ) for row in rows ] @router.post("/account-overrides", response_model=AccountOverrideResponse, status_code=status.HTTP_201_CREATED) async def create_account_override( data: AccountOverrideCreate, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(require_admin)], ): """Create an account limit override.""" # Look up account by display_code result = await db.execute(select(Account).where(Account.display_code == data.account_display_code)) account = result.scalar_one_or_none() if not account: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found") # Check for existing override existing = await db.execute( select(AccountLimitOverride).where(AccountLimitOverride.account_id == account.id) ) if existing.scalar_one_or_none(): raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Override already exists for this account") override = AccountLimitOverride( account_id=account.id, override_max_trees=data.override_max_trees, override_max_sessions_per_month=data.override_max_sessions_per_month, override_max_users=data.override_max_users, note=data.note, created_by_id=current_user.id, ) db.add(override) await log_audit(db, current_user.id, "account_override.create", "account", account.id, {"display_code": data.account_display_code}) await db.commit() await db.refresh(override) return AccountOverrideResponse( id=override.id, account_id=override.account_id, account_name=account.name, account_display_code=account.display_code, override_max_trees=override.override_max_trees, override_max_sessions_per_month=override.override_max_sessions_per_month, override_max_users=override.override_max_users, note=override.note, created_at=override.created_at, updated_at=override.updated_at, ) @router.put("/account-overrides/{override_id}", response_model=AccountOverrideResponse) async def update_account_override( override_id: UUID, data: AccountOverrideUpdate, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(require_admin)], ): """Update an account limit override.""" result = await db.execute(select(AccountLimitOverride).where(AccountLimitOverride.id == override_id)) override = result.scalar_one_or_none() if not override: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Override not found") if data.override_max_trees is not None: override.override_max_trees = data.override_max_trees if data.override_max_sessions_per_month is not None: override.override_max_sessions_per_month = data.override_max_sessions_per_month if data.override_max_users is not None: override.override_max_users = data.override_max_users if data.note is not None: override.note = data.note await log_audit(db, current_user.id, "account_override.update", "account", override.account_id) await db.commit() await db.refresh(override) # Fetch account info acct = await db.execute(select(Account).where(Account.id == override.account_id)) account = acct.scalar_one_or_none() return AccountOverrideResponse( id=override.id, account_id=override.account_id, account_name=account.name if account else None, account_display_code=account.display_code if account else None, override_max_trees=override.override_max_trees, override_max_sessions_per_month=override.override_max_sessions_per_month, override_max_users=override.override_max_users, note=override.note, created_at=override.created_at, updated_at=override.updated_at, ) @router.delete("/account-overrides/{override_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_account_override( override_id: UUID, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(require_admin)], ): """Delete an account limit override.""" result = await db.execute(select(AccountLimitOverride).where(AccountLimitOverride.id == override_id)) override = result.scalar_one_or_none() if not override: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Override not found") await log_audit(db, current_user.id, "account_override.delete", "account", override.account_id) await db.delete(override) await db.commit()