"""Portfolio API routes.""" from fastapi import APIRouter, Depends, HTTPException, Request, Response, status from app.core.auth import AuthContext, Permission, authorize_request from app.core.config import Settings, get_settings from app.repositories.factory import get_portfolio_repository_from_settings from app.schemas.portfolio import ( BuyRequest, CommentCreateRequest, CommentResponse, CommentUpdateRequest, CreatePortfolioRequest, InvestRequest, PortfolioDetailResponse, PortfolioListItem, ResetPortfolioRequest, SellRequest, TransferRequest, UpdatePortfolioRequest, ) from app.services.portfolio_service import PortfolioService router = APIRouter(prefix="/portfolios", tags=["portfolios"]) def _raise_portfolio_error(exc: ValueError) -> None: detail = str(exc) if "찾을 수 없습니다" in detail: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=detail) from exc raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=detail) from exc def _get_service(settings: Settings) -> tuple[PortfolioService, object]: repository = get_portfolio_repository_from_settings(settings) return PortfolioService(repository), repository def _close_repository(repository: object) -> None: if callable(getattr(type(repository), "close", None)): repository.close() def _ownership_required(settings: Settings) -> bool: return settings.db_enabled def _require_owner_context(auth_context: AuthContext) -> str: if auth_context.user_id is None: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Ownership context is required") return auth_context.user_id def _ensure_portfolio_owned(repository: object, portfolio_id: str, auth_context: AuthContext) -> None: if auth_context.permission == Permission.ADMIN: return owner_user_id = _require_owner_context(auth_context) try: portfolio = repository.get(portfolio_id) except ValueError as exc: _raise_portfolio_error(exc) if portfolio.owner_user_id != owner_user_id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Portfolio ownership mismatch") @router.get("", response_model=list[PortfolioListItem]) def list_portfolios( request: Request, broker: str | None = None, status_filter: str | None = None, settings: Settings = Depends(get_settings), ) -> list[PortfolioListItem]: auth_context = authorize_request(Permission.READ, request, settings) service, repository = _get_service(settings) try: owner_user_id: str | None = None if _ownership_required(settings) and auth_context.permission != Permission.ADMIN: owner_user_id = _require_owner_context(auth_context) return service.list_portfolios(broker=broker, status=status_filter, owner_user_id=owner_user_id) finally: _close_repository(repository) @router.get("/{portfolio_id}", response_model=PortfolioDetailResponse) def get_portfolio( portfolio_id: str, request: Request, *, recent_trades: int = 5, settings: Settings = Depends(get_settings), ) -> PortfolioDetailResponse: auth_context = authorize_request(Permission.READ, request, settings) service, repository = _get_service(settings) try: if _ownership_required(settings): _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.get_portfolio_status(portfolio_id=portfolio_id, recent_trades=recent_trades) finally: _close_repository(repository) @router.post( "", response_model=PortfolioDetailResponse, status_code=status.HTTP_201_CREATED, ) def create_portfolio( payload: CreatePortfolioRequest, request: Request, *, settings: Settings = Depends(get_settings), ) -> PortfolioDetailResponse: auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: owner_user_id = auth_context.user_id or "system" if _ownership_required(settings): owner_user_id = _require_owner_context(auth_context) return service.create_portfolio(payload, owner_user_id=owner_user_id) finally: _close_repository(repository) @router.patch("/{portfolio_id}", response_model=PortfolioDetailResponse) def update_portfolio( portfolio_id: str, payload: UpdatePortfolioRequest, request: Request, *, settings: Settings = Depends(get_settings), ) -> PortfolioDetailResponse: auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: if _ownership_required(settings): _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.update_portfolio(portfolio_id, payload) finally: _close_repository(repository) @router.post("/{portfolio_id}/buy", response_model=PortfolioDetailResponse) def buy_portfolio_asset( portfolio_id: str, payload: BuyRequest, request: Request, *, settings: Settings = Depends(get_settings), ) -> PortfolioDetailResponse: auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: if _ownership_required(settings): _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.buy(portfolio_id, payload) finally: _close_repository(repository) @router.post("/{portfolio_id}/sell", response_model=PortfolioDetailResponse) def sell_portfolio_asset( portfolio_id: str, payload: SellRequest, request: Request, *, settings: Settings = Depends(get_settings), ) -> PortfolioDetailResponse: auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: if _ownership_required(settings): _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.sell(portfolio_id, payload) finally: _close_repository(repository) @router.post("/{portfolio_id}/invest", response_model=PortfolioDetailResponse) def invest_buffer( portfolio_id: str, payload: InvestRequest, request: Request, *, settings: Settings = Depends(get_settings), ) -> PortfolioDetailResponse: auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: if _ownership_required(settings): _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.invest(portfolio_id, payload) finally: _close_repository(repository) @router.post("/{portfolio_id}/transfer", response_model=PortfolioDetailResponse) def transfer_buffer( portfolio_id: str, payload: TransferRequest, request: Request, *, settings: Settings = Depends(get_settings), ) -> PortfolioDetailResponse: auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: if _ownership_required(settings): _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.transfer(portfolio_id, payload) finally: _close_repository(repository) @router.post("/{portfolio_id}/reset", response_model=PortfolioDetailResponse) def reset_portfolio( portfolio_id: str, payload: ResetPortfolioRequest, request: Request, *, settings: Settings = Depends(get_settings), ) -> PortfolioDetailResponse: auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: if _ownership_required(settings): _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.reset(portfolio_id, payload) finally: _close_repository(repository) @router.delete("/{portfolio_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_portfolio( portfolio_id: str, request: Request, *, keep_ledger: bool = False, settings: Settings = Depends(get_settings), ) -> Response: authorize_request(Permission.ADMIN, request, settings) service, repository = _get_service(settings) try: service.delete(portfolio_id, keep_ledger=keep_ledger) finally: _close_repository(repository) return Response(status_code=status.HTTP_204_NO_CONTENT) @router.post( "/{portfolio_id}/comments", response_model=CommentResponse, status_code=status.HTTP_201_CREATED, ) def create_comment( portfolio_id: str, payload: CommentCreateRequest, request: Request, *, settings: Settings = Depends(get_settings), ) -> CommentResponse: auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: if _ownership_required(settings): _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.create_comment(portfolio_id, payload) except ValueError as exc: _raise_portfolio_error(exc) finally: _close_repository(repository) @router.get("/{portfolio_id}/comments", response_model=list[CommentResponse]) def list_comments( portfolio_id: str, request: Request, *, settings: Settings = Depends(get_settings), ) -> list[CommentResponse]: auth_context = authorize_request(Permission.READ, request, settings) service, repository = _get_service(settings) try: if _ownership_required(settings): _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.list_comments(portfolio_id) except ValueError as exc: _raise_portfolio_error(exc) finally: _close_repository(repository) @router.patch("/{portfolio_id}/comments/{comment_id}", response_model=CommentResponse) def update_comment( portfolio_id: str, comment_id: str, payload: CommentUpdateRequest, request: Request, *, settings: Settings = Depends(get_settings), ) -> CommentResponse: auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: if _ownership_required(settings): _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.update_comment(portfolio_id, comment_id, payload) except ValueError as exc: _raise_portfolio_error(exc) finally: _close_repository(repository) @router.delete("/{portfolio_id}/comments/{comment_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_comment( portfolio_id: str, comment_id: str, request: Request, *, settings: Settings = Depends(get_settings), ) -> Response: auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: if _ownership_required(settings): _ensure_portfolio_owned(repository, portfolio_id, auth_context) service.delete_comment(portfolio_id, comment_id) except ValueError as exc: _raise_portfolio_error(exc) finally: _close_repository(repository) return Response(status_code=status.HTTP_204_NO_CONTENT)