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:
@@ -160,9 +160,15 @@ class _FakeRepo:
|
||||
|
||||
def get_return_summary(self, portfolio_id: str) -> dict[str, float]:
|
||||
self.get(portfolio_id)
|
||||
invested = sum(e.price for e in self.entries if e.action == "INVEST")
|
||||
withdrawn = sum(e.price for e in self.entries if e.action == "WITHDRAW")
|
||||
profit = sum(e.price for e in self.entries if e.action == "PROFIT")
|
||||
reversed_ids = {
|
||||
e.target_ledger_entry_id
|
||||
for e in self.entries
|
||||
if e.action == "REVERSAL" and e.target_ledger_entry_id is not None
|
||||
}
|
||||
active = [e for e in self.entries if e.ledger_entry_id not in reversed_ids]
|
||||
invested = sum(e.price for e in active if e.action == "INVEST")
|
||||
withdrawn = sum(e.price for e in active if e.action == "WITHDRAW")
|
||||
profit = sum(e.price for e in active if e.action == "PROFIT")
|
||||
return {
|
||||
"total_invested": invested,
|
||||
"total_withdrawn": withdrawn,
|
||||
@@ -257,6 +263,40 @@ def test_profit_is_exposed_separately_from_net_contributed_capital() -> None:
|
||||
assert data["cumulative_profit_withdrawn"] == 50.0
|
||||
|
||||
|
||||
def test_return_summary_excludes_reversed_invest_withdraw_and_profit() -> None:
|
||||
repo = _FakeRepo(_portfolio(buffer=1_000))
|
||||
client = _make_client(repo)
|
||||
|
||||
entries = []
|
||||
for action, amount in (("INVEST", 300), ("WITHDRAW", 100), ("PROFIT", 50)):
|
||||
response = client.post(
|
||||
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
|
||||
json={"action": action, "amount": amount},
|
||||
headers=_auth(),
|
||||
)
|
||||
assert response.status_code == 201
|
||||
entries.append(response.json()["ledger_entry_id"])
|
||||
|
||||
for target in entries:
|
||||
response = client.post(
|
||||
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
|
||||
json={"action": "REVERSAL", "target_ledger_entry_id": target},
|
||||
headers=_auth(),
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
resp = client.get(f"/v1/portfolios/{_PORTFOLIO_ID}/returns/summary", headers=_auth())
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == {
|
||||
"portfolio_id": _PORTFOLIO_ID,
|
||||
"total_invested": 0.0,
|
||||
"total_withdrawn": 0.0,
|
||||
"net_contributed_capital": 0.0,
|
||||
"cumulative_profit_withdrawn": 0.0,
|
||||
}
|
||||
|
||||
|
||||
def test_insufficient_cash_rejected_as_409() -> None:
|
||||
repo = _FakeRepo(_portfolio(cash=10, quantity=0, price=0))
|
||||
client = _make_client(repo)
|
||||
|
||||
Reference in New Issue
Block a user