"""Company routes — minimal for cross-tenant and RBAC tests.""" from __future__ import annotations import uuid from typing import Any from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.audit import log_audit from app.core.db import get_db from app.core.auth import check_permission, filter_fields_by_permission from app.deps import get_current_user, require_admin from app.models.company import Company from app.models.role import Role from app.schemas.company import CompanyCreate router = APIRouter(prefix="/api/v1/companies", tags=["companies"]) @router.get("") async def list_companies( db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """List companies in current tenant.""" tenant_id = uuid.UUID(current_user["tenant_id"]) q = select(Company).where( Company.tenant_id == tenant_id, Company.deleted_at.is_(None), ) result = await db.execute(q) companies = result.scalars().all() return {"items": [_company_to_dict(c) for c in companies]} @router.post("") async def create_company( body: CompanyCreate, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Create a company. Requires write permission.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) role = current_user.get("role", "viewer") # RBAC check if not check_permission(role, "companies", "create"): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail={"detail": "Insufficient permissions", "code": "forbidden"}, ) company = Company( tenant_id=tenant_id, name=body.name, industry=body.industry, phone=body.phone, email=body.email, website=body.website, description=body.description, created_by=user_id, updated_by=user_id, ) db.add(company) await db.flush() # Audit log await log_audit( db, tenant_id, user_id, "create", "company", company.id, changes={"name": body.name}, ) return _company_to_dict(company) @router.get("/{company_id}") async def get_company( company_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Get a single company. Cross-tenant access returns 404.""" tenant_id = uuid.UUID(current_user["tenant_id"]) try: cid = uuid.UUID(company_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid company_id", "code": "invalid_id"}) q = select(Company).where( Company.id == cid, Company.tenant_id == tenant_id, # Tenant filter = cross-tenant returns 404 Company.deleted_at.is_(None), ) result = await db.execute(q) company = result.scalar_one_or_none() if company is None: raise HTTPException(404, detail={"detail": "Company not found", "code": "not_found"}) # Field-level permissions data = _company_to_dict(company) # Check if user's role has field permissions defined role = current_user.get("role", "viewer") if role != "admin": # Check for custom role field permissions role_q = select(Role).where(Role.tenant_id == tenant_id, Role.name == role) role_result = await db.execute(role_q) custom_role = role_result.scalar_one_or_none() if custom_role and custom_role.field_permissions: data = filter_fields_by_permission(data, custom_role.field_permissions, role) return data @router.patch("/{company_id}") async def update_company( company_id: str, body: CompanyCreate, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Update a company.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) role = current_user.get("role", "viewer") if not check_permission(role, "companies", "update"): raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"}) try: cid = uuid.UUID(company_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid company_id", "code": "invalid_id"}) q = select(Company).where(Company.id == cid, Company.tenant_id == tenant_id, Company.deleted_at.is_(None)) result = await db.execute(q) company = result.scalar_one_or_none() if company is None: raise HTTPException(404, detail={"detail": "Company not found", "code": "not_found"}) changes: dict[str, Any] = {} if body.name is not None: changes["name"] = {"old": company.name, "new": body.name} company.name = body.name if body.industry is not None: company.industry = body.industry if body.phone is not None: company.phone = body.phone if body.email is not None: company.email = body.email if body.website is not None: company.website = body.website if body.description is not None: company.description = body.description company.updated_by = user_id await db.flush() await log_audit(db, tenant_id, user_id, "update", "company", cid, changes=changes) return _company_to_dict(company) @router.delete("/{company_id}") async def delete_company( company_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Soft-delete a company.""" from datetime import datetime, timezone tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) role = current_user.get("role", "viewer") if not check_permission(role, "companies", "delete"): raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"}) try: cid = uuid.UUID(company_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid company_id", "code": "invalid_id"}) q = select(Company).where(Company.id == cid, Company.tenant_id == tenant_id, Company.deleted_at.is_(None)) result = await db.execute(q) company = result.scalar_one_or_none() if company is None: raise HTTPException(404, detail={"detail": "Company not found", "code": "not_found"}) company.deleted_at = datetime.now(timezone.utc) company.updated_by = user_id await log_audit(db, tenant_id, user_id, "delete", "company", cid, changes={"name": company.name}) from fastapi import Response return Response(status_code=status.HTTP_204_NO_CONTENT) def _company_to_dict(c: Company) -> dict[str, Any]: """Convert company to response dict.""" return { "id": str(c.id), "name": c.name, "industry": c.industry, "phone": c.phone, "email": c.email, "annual_revenue": None, # Minimal model — would be from DB column }