"""Capital account statements: admin entry, investor read of their own figures.""" from fastapi import APIRouter, Depends, HTTPException from sqlmodel import Session, select, col from ten31portal.audit import record_audit from ten31portal.auth import ( accessible_entity_ids, get_current_user, household_user_ids, require_internal_admin, ) from ten31portal.database import get_session from ten31portal.models import ( CapitalAccountStatement, Entity, EntityAccess, User, UserRole, ) from ten31portal.schemas import CapitalAccountCreate, CapitalAccountResponse router = APIRouter(prefix="/api/capital-accounts", tags=["capital-accounts"]) def _dollars_to_cents(dollars: float) -> int: return round(dollars * 100) def exit_dates(session: Session, rows) -> dict: """(investor_user_id, entity_id) -> exited_on for every exited pair in these statements, so a sold/transferred stake renders as "Exited" instead of a phantom -100% loss.""" if not rows: return {} return { (a.user_id, a.entity_id): a.exited_on for a in session.exec( select(EntityAccess).where( col(EntityAccess.user_id).in_({r.investor_user_id for r in rows}), col(EntityAccess.exited_on).is_not(None), ) ).all() } @router.get("") def list_statements( entity_id: int | None = None, investor_user_id: int | None = None, user: User = Depends(get_current_user), session: Session = Depends(get_session), ) -> list[CapitalAccountResponse]: query = select(CapitalAccountStatement) allowed = accessible_entity_ids(user, session) if allowed is not None: # External accounts see statements for every legal name linked to their login. query = query.where( col(CapitalAccountStatement.investor_user_id).in_(household_user_ids(user, session)) ) if allowed: query = query.where(col(CapitalAccountStatement.entity_id).in_(allowed)) else: return [] else: if investor_user_id is not None: query = query.where(CapitalAccountStatement.investor_user_id == investor_user_id) if entity_id is not None: query = query.where(CapitalAccountStatement.entity_id == entity_id) rows = session.exec( query.order_by(col(CapitalAccountStatement.as_of_date).desc()) ).all() # Attach each statement's legal name so the portal can label/group accounts held under # different names without an admin-only user lookup. names = dict(session.exec( select(User.id, User.name).where( col(User.id).in_({r.investor_user_id for r in rows}) ) ).all()) if rows else {} exits = exit_dates(session, rows) out: list[CapitalAccountResponse] = [] for r in rows: data = CapitalAccountResponse.model_validate(r, from_attributes=True) data.investor_name = names.get(r.investor_user_id) data.exited_on = exits.get((r.investor_user_id, r.entity_id)) out.append(data) return out @router.post("", status_code=201) def create_statement( body: CapitalAccountCreate, admin: User = Depends(require_internal_admin), session: Session = Depends(get_session), ) -> CapitalAccountResponse: if session.get(Entity, body.entity_id) is None: raise HTTPException(status_code=404, detail="Entity not found") investor = session.get(User, body.investor_user_id) if investor is None or investor.role != UserRole.investor: raise HTTPException(status_code=400, detail="investor_user_id must be an investor account") existing = session.exec( select(CapitalAccountStatement).where( CapitalAccountStatement.entity_id == body.entity_id, CapitalAccountStatement.investor_user_id == body.investor_user_id, CapitalAccountStatement.as_of_date == body.as_of_date, ) ).first() if existing: raise HTTPException( status_code=409, detail="A statement for this investor, fund, and date already exists.", ) stmt = CapitalAccountStatement( entity_id=body.entity_id, investor_user_id=body.investor_user_id, as_of_date=body.as_of_date, commitment_cents=_dollars_to_cents(body.commitment_dollars), beginning_balance_cents=_dollars_to_cents(body.beginning_balance_dollars), contributions_cents=_dollars_to_cents(body.contributions_dollars), distributions_cents=_dollars_to_cents(body.distributions_dollars), ending_balance_cents=_dollars_to_cents(body.ending_balance_dollars), document_id=body.document_id, ) session.add(stmt) session.flush() record_audit(session, admin.id, "create", "capital_account", stmt.id, { "entity_id": body.entity_id, "investor_user_id": body.investor_user_id, "as_of_date": str(body.as_of_date), "ending_balance_cents": stmt.ending_balance_cents, }) session.commit() session.refresh(stmt) return CapitalAccountResponse.model_validate(stmt, from_attributes=True) @router.delete("/{statement_id}") def delete_statement( statement_id: int, admin: User = Depends(require_internal_admin), session: Session = Depends(get_session), ) -> dict[str, str]: stmt = session.get(CapitalAccountStatement, statement_id) if stmt is None: raise HTTPException(status_code=404, detail="Statement not found") record_audit(session, admin.id, "delete", "capital_account", statement_id, None) session.delete(stmt) session.commit() return {"status": "deleted"}