feat(req-002): implement portfolio tagging and tag filters
This commit is contained in:
@@ -7,7 +7,7 @@ import json
|
||||
from fastapi import Depends
|
||||
|
||||
from app.core.config import Settings, get_settings
|
||||
from app.domain.models import LedgerEntry, Portfolio, PortfolioComment, StrategySet
|
||||
from app.domain.models import LedgerEntry, Portfolio, PortfolioComment, PortfolioTag, StrategySet
|
||||
from app.domain.services import (
|
||||
apply_buy,
|
||||
apply_delete,
|
||||
@@ -28,6 +28,8 @@ from app.schemas.portfolio import (
|
||||
InvestRequest,
|
||||
PortfolioDetailResponse,
|
||||
PortfolioListItem,
|
||||
PortfolioTagMutationRequest,
|
||||
PortfolioTagResponse,
|
||||
ResetPortfolioRequest,
|
||||
SellRequest,
|
||||
TransferRequest,
|
||||
@@ -49,6 +51,7 @@ def _to_portfolio_summary(portfolio: Portfolio):
|
||||
status=portfolio.status,
|
||||
name=portfolio.name,
|
||||
symbol=portfolio.symbol,
|
||||
tags=list(portfolio.tags or []),
|
||||
owner_user_id=portfolio.owner_user_id,
|
||||
cash_balance=portfolio.cash_balance,
|
||||
asset_quantity=portfolio.asset_quantity,
|
||||
@@ -121,6 +124,31 @@ def _to_comment_response(comment: PortfolioComment) -> CommentResponse:
|
||||
)
|
||||
|
||||
|
||||
def _to_tag_response(tag: PortfolioTag) -> PortfolioTagResponse:
|
||||
return PortfolioTagResponse(portfolio_id=tag.portfolio_id, tag=tag.tag, created_at=tag.created_at)
|
||||
|
||||
|
||||
def _normalize_tag(value: str) -> str:
|
||||
normalized = value.strip()
|
||||
if not normalized:
|
||||
raise ValueError("tag는 비어 있을 수 없습니다")
|
||||
return normalized
|
||||
|
||||
|
||||
def _normalize_tag_list(values: list[str] | None) -> list[str] | None:
|
||||
if not values:
|
||||
return None
|
||||
normalized: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for value in values:
|
||||
item = _normalize_tag(value)
|
||||
if item in seen:
|
||||
continue
|
||||
seen.add(item)
|
||||
normalized.append(item)
|
||||
return normalized or None
|
||||
|
||||
|
||||
def _to_detail_response(status_result) -> PortfolioDetailResponse:
|
||||
return PortfolioDetailResponse(
|
||||
portfolio=_to_portfolio_summary(status_result.portfolio),
|
||||
@@ -144,8 +172,21 @@ class PortfolioService:
|
||||
broker: str | None = None,
|
||||
status: str | None = None,
|
||||
owner_user_id: str | None = None,
|
||||
include_tags: list[str] | None = None,
|
||||
exclude_tags: list[str] | None = None,
|
||||
match_mode: str | None = None,
|
||||
) -> list[PortfolioListItem]:
|
||||
portfolios = self._repository.list_portfolios(broker=broker, status=status, owner_user_id=owner_user_id)
|
||||
normalized_mode = None if match_mode is None else match_mode.upper()
|
||||
if normalized_mode is not None and normalized_mode not in {"AND", "OR"}:
|
||||
raise ValueError("matchMode는 AND 또는 OR 여야 합니다")
|
||||
portfolios = self._repository.list_portfolios(
|
||||
broker=broker,
|
||||
status=status,
|
||||
owner_user_id=owner_user_id,
|
||||
include_tags=_normalize_tag_list(include_tags),
|
||||
exclude_tags=_normalize_tag_list(exclude_tags),
|
||||
match_mode=normalized_mode,
|
||||
)
|
||||
return [
|
||||
PortfolioListItem(
|
||||
**_to_portfolio_summary(portfolio).model_dump(),
|
||||
@@ -302,6 +343,19 @@ class PortfolioService:
|
||||
self._repository.get(portfolio_id)
|
||||
self._repository.delete_comment(portfolio_id, comment_id)
|
||||
|
||||
def add_tag(self, portfolio_id: str, payload: PortfolioTagMutationRequest) -> PortfolioTagResponse:
|
||||
self._repository.get(portfolio_id)
|
||||
tag = self._repository.add_tag(portfolio_id, tag=_normalize_tag(payload.tag))
|
||||
return _to_tag_response(tag)
|
||||
|
||||
def list_tags(self, portfolio_id: str) -> list[PortfolioTagResponse]:
|
||||
self._repository.get(portfolio_id)
|
||||
return [_to_tag_response(tag) for tag in self._repository.list_tags(portfolio_id)]
|
||||
|
||||
def remove_tag(self, portfolio_id: str, payload: PortfolioTagMutationRequest) -> None:
|
||||
self._repository.get(portfolio_id)
|
||||
self._repository.remove_tag(portfolio_id, tag=_normalize_tag(payload.tag))
|
||||
|
||||
|
||||
def get_portfolio_service(
|
||||
repository: PortfolioRepository = Depends(get_portfolio_repository),
|
||||
|
||||
Reference in New Issue
Block a user