98 lines
3.8 KiB
Python
98 lines
3.8 KiB
Python
"""Repository contract tests for portfolio ledger return aggregates."""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from app.repositories.postgres import PostgresPortfolioRepository
|
|
|
|
_ROOT = Path(__file__).parents[1]
|
|
|
|
|
|
def _repo_with_cursor(rows: list[tuple[object, ...]]):
|
|
repo = PostgresPortfolioRepository.__new__(PostgresPortfolioRepository)
|
|
repo.ensure_schema = MagicMock()
|
|
cursor = MagicMock()
|
|
cursor.__enter__.return_value = cursor
|
|
cursor.__exit__.return_value = None
|
|
cursor.fetchall.return_value = rows
|
|
connection = MagicMock()
|
|
connection.cursor.return_value = cursor
|
|
return repo, connection, cursor
|
|
|
|
|
|
def test_get_return_summaries_empty_ids_skips_database() -> None:
|
|
repo = PostgresPortfolioRepository.__new__(PostgresPortfolioRepository)
|
|
repo.ensure_schema = MagicMock()
|
|
|
|
with patch.object(repo, "_connect") as connect:
|
|
result = repo.get_return_summaries([])
|
|
|
|
assert result == {}
|
|
connect.assert_not_called()
|
|
repo.ensure_schema.assert_not_called()
|
|
|
|
|
|
def test_get_return_summaries_groups_all_sources_and_excludes_reversed_entries() -> None:
|
|
repo, connection, cursor = _repo_with_cursor(
|
|
[
|
|
("p-001", 500.0, 125.0, 40.0),
|
|
("p-002", 200.0, 0.0, 5.0),
|
|
]
|
|
)
|
|
|
|
with patch.object(repo, "_connect", return_value=connection):
|
|
result = repo.get_return_summaries(["p-001", "p-002"])
|
|
|
|
assert result == {
|
|
"p-001": {
|
|
"total_invested": 500.0,
|
|
"total_withdrawn": 125.0,
|
|
"net_contributed_capital": 375.0,
|
|
"cumulative_profit_withdrawn": 40.0,
|
|
},
|
|
"p-002": {
|
|
"total_invested": 200.0,
|
|
"total_withdrawn": 0.0,
|
|
"net_contributed_capital": 200.0,
|
|
"cumulative_profit_withdrawn": 5.0,
|
|
},
|
|
}
|
|
cursor.execute.assert_called_once()
|
|
sql, params = cursor.execute.call_args.args
|
|
normalized_sql = " ".join(sql.split()).lower()
|
|
assert "group by ledger.portfolio_id" in normalized_sql
|
|
assert "not exists" in normalized_sql
|
|
assert "reversal.target_ledger_entry_id = ledger.ledger_entry_id" in normalized_sql
|
|
assert "ledger.source = 'manual'" not in normalized_sql
|
|
assert "reversal.source = 'manual'" not in normalized_sql
|
|
assert "ledger.side in ('invest', 'withdraw', 'profit')" in normalized_sql
|
|
assert "sum(ledger.price) filter (where ledger.side = 'invest')" in normalized_sql
|
|
assert "sum(ledger.price) filter (where ledger.side = 'withdraw')" in normalized_sql
|
|
assert "sum(ledger.price) filter (where ledger.side = 'profit')" in normalized_sql
|
|
assert params == (["p-001", "p-002"],)
|
|
|
|
|
|
def test_get_return_summary_delegates_to_shared_batch_semantics() -> None:
|
|
repo = PostgresPortfolioRepository.__new__(PostgresPortfolioRepository)
|
|
expected = {
|
|
"total_invested": 300.0,
|
|
"total_withdrawn": 100.0,
|
|
"net_contributed_capital": 200.0,
|
|
"cumulative_profit_withdrawn": 50.0,
|
|
}
|
|
repo.get_return_summaries = MagicMock(return_value={"p-001": expected})
|
|
|
|
assert repo.get_return_summary("p-001") == expected
|
|
repo.get_return_summaries.assert_called_once_with(["p-001"])
|
|
|
|
|
|
def test_external_contract_documents_all_source_return_semantics() -> None:
|
|
contract = (_ROOT / "docs/interface-contract.md").read_text()
|
|
|
|
assert "source와 무관한 유효 `INVEST`의 gross 합계" in contract
|
|
assert "`net_contributed_capital` (**순투자원금**) = `total_invested - total_withdrawn`" in contract
|
|
assert "`PROFIT`는 원금 계산에서 제외" in contract
|
|
assert "original과 reversal의 source와 무관하게 제외" in contract
|
|
assert "`buffer_out` 등 `INVEST`/`WITHDRAW`/`PROFIT` 이외 side" in contract
|
|
assert "유효 manual `INVEST` 합계" not in contract
|
|
assert "유효 manual `PROFIT` 합계" not in contract |