"""Entity CRUD and rollup endpoints.""" from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy import func, literal from sqlmodel import Session, select from ten31portal.audit import record_audit from ten31portal.auth import get_current_user, require_writer from ten31portal.database import get_session from ten31portal.models import ( Entity, EntityStatus, Holding, Position, Valuation, ValuationRound, RoundStatus, User, ) from ten31portal.schemas import EntityCreate, EntityResponse, EntityUpdate router = APIRouter(prefix="/api/entities", tags=["entities"]) class EntityRollupItem(BaseModel): id: int name: str type: str vintage_year: int | None fund_size_cents: int | None status: str invested_cents: int last_signed_value_cents: int @router.get("/rollup") def entity_rollup( user: User = Depends(get_current_user), session: Session = Depends(get_session), ) -> list[EntityRollupItem]: """Per-entity invested and last-signed-value in a single pass.""" entities = session.exec(select(Entity)).all() result: list[EntityRollupItem] = [] for ent in entities: # Sum cost_cents across all positions in this entity's holdings invested_row = session.exec( select(func.coalesce(func.sum(Position.cost_cents), 0)) .join(Holding, Position.holding_id == Holding.id) .where(Holding.entity_id == ent.id) ).one() invested_cents = int(invested_row) # Find the latest approved round for this entity latest_round = session.exec( select(ValuationRound) .where( ValuationRound.entity_id == ent.id, ValuationRound.status == RoundStatus.approved, ) .order_by(ValuationRound.quarter_end.desc()) # type: ignore[union-attr] ).first() last_signed_value_cents = 0 if latest_round: val_sum = session.exec( select(func.coalesce(func.sum(Valuation.value_cents), 0)) .where(Valuation.round_id == latest_round.id) ).one() last_signed_value_cents = int(val_sum) result.append(EntityRollupItem( id=ent.id, name=ent.name, type=ent.type.value, vintage_year=ent.vintage_year, fund_size_cents=ent.fund_size_cents, status=ent.status.value, invested_cents=invested_cents, last_signed_value_cents=last_signed_value_cents, )) return result @router.get("") def list_entities( user: User = Depends(get_current_user), session: Session = Depends(get_session), ) -> list[EntityResponse]: rows = session.exec(select(Entity)).all() return [EntityResponse.model_validate(r, from_attributes=True) for r in rows] @router.get("/{entity_id}") def get_entity( entity_id: int, user: User = Depends(get_current_user), session: Session = Depends(get_session), ) -> EntityResponse: entity = session.get(Entity, entity_id) if entity is None: raise HTTPException(status_code=404, detail="Entity not found") return EntityResponse.model_validate(entity, from_attributes=True) @router.post("", status_code=201) def create_entity( body: EntityCreate, user: User = Depends(require_writer), session: Session = Depends(get_session), ) -> EntityResponse: entity = Entity(**body.model_dump()) session.add(entity) session.flush() record_audit(session, user.id, "create", "entity", entity.id, body.model_dump()) session.commit() session.refresh(entity) return EntityResponse.model_validate(entity, from_attributes=True) @router.patch("/{entity_id}") def update_entity( entity_id: int, body: EntityUpdate, user: User = Depends(require_writer), session: Session = Depends(get_session), ) -> EntityResponse: entity = session.get(Entity, entity_id) if entity is None: raise HTTPException(status_code=404, detail="Entity not found") changes = body.model_dump(exclude_unset=True) for key, val in changes.items(): setattr(entity, key, val) session.add(entity) session.flush() record_audit(session, user.id, "update", "entity", entity.id, changes) session.commit() session.refresh(entity) return EntityResponse.model_validate(entity, from_attributes=True)