"""v1 portfolio tag endpoints — apply and remove tags (REQ-005).""" from __future__ import annotations import logging from fastapi import APIRouter, Depends, HTTPException, status from app.core.auth import AuthContext, Permission, require_trade_write from app.repositories.factory import get_portfolio_repository_from_settings from app.schemas.v1 import TagApplyRequest, TagApplyResponse logger = logging.getLogger(__name__) router = APIRouter(prefix="/v1/portfolios", tags=["v1"]) def _get_repo(settings=None): from app.core.config import get_settings return get_portfolio_repository_from_settings(settings or get_settings()) def _close_repo(repo) -> None: if callable(getattr(type(repo), "close", None)): repo.close() @router.post("/{portfolio_id}/tags", response_model=TagApplyResponse) def apply_portfolio_tag( portfolio_id: str, body: TagApplyRequest, auth: AuthContext = Depends(require_trade_write), ) -> TagApplyResponse: if auth.token_source != "db": raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="DB token required") repo = _get_repo() try: try: portfolio = repo.get(portfolio_id) except ValueError: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"portfolio를 찾을 수 없습니다: {portfolio_id}") is_admin = auth.permission == Permission.ADMIN is_owner = portfolio.owner_user_id == auth.user_id if not is_admin and not is_owner: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Portfolio ownership mismatch") tag = repo.get_tag_by_id(body.tag_id) if tag is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"태그를 찾을 수 없습니다: {body.tag_id}") if not tag.is_active: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="비활성화된 태그는 적용할 수 없습니다") if not is_admin and not tag.is_admin_tag and tag.created_by_user_id != portfolio.owner_user_id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="태그 적용 정책을 위반했습니다") repo.append_portfolio_tag_id(portfolio_id, body.tag_id) except HTTPException: raise except ValueError as exc: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc finally: _close_repo(repo) return TagApplyResponse(tag_id=body.tag_id, status="applied") @router.delete("/{portfolio_id}/tags/{tag_id}", response_model=TagApplyResponse) def remove_portfolio_tag( portfolio_id: str, tag_id: str, auth: AuthContext = Depends(require_trade_write), ) -> TagApplyResponse: if auth.token_source != "db": raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="DB token required") repo = _get_repo() try: try: portfolio = repo.get(portfolio_id) except ValueError: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"portfolio를 찾을 수 없습니다: {portfolio_id}") is_admin = auth.permission == Permission.ADMIN is_owner = portfolio.owner_user_id == auth.user_id if not is_admin and not is_owner: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Portfolio ownership mismatch") repo.remove_portfolio_tag_id(portfolio_id, tag_id) except HTTPException: raise except ValueError as exc: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc finally: _close_repo(repo) return TagApplyResponse(tag_id=tag_id, status="removed")