feat(portfolios): expose ledger investment aggregates
Add reversal-aware grouped manual-ledger aggregates to portfolio list responses and align the single-portfolio return summary with the same semantics.\n\nRefs #23
This commit is contained in:
@@ -81,6 +81,8 @@ class _FakeListRepo:
|
||||
) -> None:
|
||||
self._portfolios: list[Portfolio] = portfolios or []
|
||||
self._token_principal = token_principal
|
||||
self.return_summaries: dict[str, dict[str, float]] = {}
|
||||
self.summary_requests: list[list[str]] = []
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
@@ -106,6 +108,14 @@ class _FakeListRepo:
|
||||
results = [p for p in results if p.status == status]
|
||||
return results
|
||||
|
||||
def get_return_summaries(self, portfolio_ids: list[str]) -> dict[str, dict[str, float]]:
|
||||
self.summary_requests.append(list(portfolio_ids))
|
||||
return {
|
||||
portfolio_id: self.return_summaries[portfolio_id]
|
||||
for portfolio_id in portfolio_ids
|
||||
if portfolio_id in self.return_summaries
|
||||
}
|
||||
|
||||
|
||||
def _make_client(
|
||||
admin_tokens: list[str] | None = None,
|
||||
@@ -296,6 +306,61 @@ def test_get_response_has_asset_id_field() -> None:
|
||||
assert item["symbol"] == "005930"
|
||||
|
||||
|
||||
def test_get_list_includes_return_summary_fields_from_one_filtered_batch() -> None:
|
||||
own = _make_portfolio("p-001", owner=_USER_ID)
|
||||
other = _make_portfolio("p-002", owner=_OTHER_USER_ID)
|
||||
repo = _FakeListRepo(portfolios=[own, other])
|
||||
repo.return_summaries = {
|
||||
"p-001": {
|
||||
"total_invested": 500.0,
|
||||
"total_withdrawn": 125.0,
|
||||
"net_contributed_capital": 375.0,
|
||||
"cumulative_profit_withdrawn": 40.0,
|
||||
},
|
||||
"p-002": {
|
||||
"total_invested": 999.0,
|
||||
"total_withdrawn": 0.0,
|
||||
"net_contributed_capital": 999.0,
|
||||
"cumulative_profit_withdrawn": 0.0,
|
||||
},
|
||||
}
|
||||
client = _make_client()
|
||||
|
||||
with _with_repo(repo):
|
||||
resp = client.get(f"/v1/portfolios?user_id={_USER_ID}", headers=_auth("admin-tok"))
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert repo.summary_requests == [["p-001"]]
|
||||
item = resp.json()["portfolios"][0]
|
||||
assert item["net_contributed_capital"] == 375.0
|
||||
assert item["cumulative_profit_withdrawn"] == 40.0
|
||||
|
||||
|
||||
def test_get_list_defaults_missing_return_summary_to_zero() -> None:
|
||||
repo = _FakeListRepo(portfolios=[_make_portfolio("p-001")])
|
||||
client = _make_client()
|
||||
|
||||
with _with_repo(repo):
|
||||
resp = client.get("/v1/portfolios", headers=_auth("admin-tok"))
|
||||
|
||||
assert resp.status_code == 200
|
||||
item = resp.json()["portfolios"][0]
|
||||
assert item["net_contributed_capital"] == 0.0
|
||||
assert item["cumulative_profit_withdrawn"] == 0.0
|
||||
|
||||
|
||||
def test_get_empty_list_uses_empty_batch_without_n_plus_one() -> None:
|
||||
repo = _FakeListRepo()
|
||||
client = _make_client()
|
||||
|
||||
with _with_repo(repo):
|
||||
resp = client.get("/v1/portfolios", headers=_auth("admin-tok"))
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == {"portfolios": [], "total": 0}
|
||||
assert repo.summary_requests == [[]]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# REQ-055: GET /v1/portfolios 응답 전략·시장 메타데이터 additive 필드 (BAT-023)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user